diff --git a/core/modules/auto_updates/auto_updates.install b/core/modules/auto_updates/auto_updates.install index 81c78c35b0..6799a1c510 100644 --- a/core/modules/auto_updates/auto_updates.install +++ b/core/modules/auto_updates/auto_updates.install @@ -17,53 +17,54 @@ function auto_updates_requirements($phase) { return []; } - $requirements = []; /** @var \Drupal\auto_updates\ReadinessChecker\ReadinessCheckerManagerInterface $checker_manager */ $checker_manager = \Drupal::service('auto_updates.readiness_checker'); if (!$checker_manager->isEnabled()) { return []; } - + $requirements['auto_updates_readiness']['title'] = t('Update readiness checks'); + $readiness_check = Url::fromRoute('auto_updates.update_readiness'); $last_check_timestamp = $checker_manager->timestamp(); - $requirements['auto_updates_readiness'] = [ - 'title' => t('Update readiness checks'), - 'severity' => REQUIREMENT_OK, - 'value' => t('Your site is ready to for automatic updates.', [':readiness_checks' => 'https://www.drupal.org/docs/8/update/automatic-updates#readiness-checks']), - ]; - $error_results = $checker_manager->getResults(ReadinessCheckerManagerInterface::ERROR); - $warning_results = $checker_manager->getResults(ReadinessCheckerManagerInterface::WARNING); - $checker_results = array_merge($error_results, $warning_results); - if (!empty($checker_results)) { - $requirements['auto_updates_readiness']['severity'] = $error_results ? REQUIREMENT_ERROR : REQUIREMENT_WARNING; - $requirements['auto_updates_readiness']['value'] = new PluralTranslatableMarkup(count($checker_results), '@count check failed:', '@count checks failed:'); - $requirements['auto_updates_readiness']['description'] = [ - '#theme' => 'item_list', - '#items' => $checker_results, - ]; + if ($last_check_timestamp === NULL) { + $requirements['auto_updates_readiness']['severity'] = REQUIREMENT_ERROR; + $requirements['auto_updates_readiness']['value'] = t('Your site has never checked if it is ready to apply automatic updates.', ['@readiness_checks' => 'https://www.drupal.org/docs/8/update/automatic-updates#readiness-checks']); + if ($readiness_check->access()) { + $requirements['auto_updates_readiness']['description'] = t('Run readiness checks manually.', [ + '@link' => $readiness_check->toString(), + ]); + } } - if ($last_check_timestamp === NULL || \Drupal::time()->getRequestTime() > $last_check_timestamp + ReadinessCheckerManagerInterface::LAST_CHECKED_WARNING) { + elseif (\Drupal::time()->getRequestTime() > $last_check_timestamp + ReadinessCheckerManagerInterface::LAST_CHECKED_WARNING) { $requirements['auto_updates_readiness']['severity'] = REQUIREMENT_ERROR; - $readiness_check = Url::fromRoute('auto_updates.update_readiness'); $time_ago = \Drupal::service('date.formatter')->formatTimeDiffSince($last_check_timestamp); - if ($last_check_timestamp === NULL) { - $requirements['auto_updates_readiness']['value'] = t('Your site has never checked if it is ready to apply automatic updates.', ['@readiness_checks' => 'https://www.drupal.org/docs/8/update/automatic-updates#readiness-checks']); + $requirements['auto_updates_readiness']['value'] = t('Your site has not recently checked if it is ready to apply automatic updates.', ['@readiness_checks' => 'https://www.drupal.org/docs/8/update/automatic-updates#readiness-checks']); + if ($readiness_check->access()) { + $requirements['auto_updates_readiness']['description'] = t('Readiness checks were last run @time ago. Run readiness checks manually.', [ + '@time' => $time_ago, + '@link' => $readiness_check->toString(), + ]); } else { - $requirements['auto_updates_readiness']['value'] = t('Your site has not recently checked if it is ready to apply automatic updates.', ['@readiness_checks' => 'https://www.drupal.org/docs/8/update/automatic-updates#readiness-checks']); $requirements['auto_updates_readiness']['description'] = t('Readiness checks were last run @time ago.', ['@time' => $time_ago]); } - if ($readiness_check->access()) { - if ($last_check_timestamp === NULL) { - $requirements['auto_updates_readiness']['description'] = t('Run readiness checks manually.', [ - '@link' => $readiness_check->toString(), - ]); - } - else { - $requirements['auto_updates_readiness']['description'] = t('Last run @time ago. Run readiness checks manually.', [ - '@time' => $time_ago, - '@link' => $readiness_check->toString(), - ]); - } + } + else { + $error_results = $checker_manager->getResults(ReadinessCheckerManagerInterface::ERROR); + $warning_results = $checker_manager->getResults(ReadinessCheckerManagerInterface::WARNING); + $checker_results = array_merge($error_results, $warning_results); + if (!empty($checker_results)) { + $requirements['auto_updates_readiness']['severity'] = $error_results ? REQUIREMENT_ERROR : REQUIREMENT_WARNING; + $requirements['auto_updates_readiness']['value'] = new PluralTranslatableMarkup(count($checker_results), '@count check failed:', '@count checks failed:'); + $requirements['auto_updates_readiness']['description'] = [ + '#theme' => 'item_list', + '#items' => $checker_results, + ]; + } + else { + $requirements['auto_updates_readiness'] += [ + 'severity' => REQUIREMENT_OK, + 'value' => t('Your site is ready for automatic updates.', [':readiness_checks' => 'https://www.drupal.org/docs/8/update/automatic-updates#readiness-checks']), + ]; } } return $requirements; diff --git a/core/modules/auto_updates/tests/src/Functional/ReadinessCheckerTest.php b/core/modules/auto_updates/tests/src/Functional/ReadinessCheckerTest.php index 79a01454d2..99d81d968e 100644 --- a/core/modules/auto_updates/tests/src/Functional/ReadinessCheckerTest.php +++ b/core/modules/auto_updates/tests/src/Functional/ReadinessCheckerTest.php @@ -18,14 +18,32 @@ class ReadinessCheckerTest extends BrowserTestBase { */ protected $defaultTheme = 'stark'; + /** + * A user who can view the status report. + * + * @var bool|\Drupal\user\Entity\User + */ + protected $reportViewerUser; + + /** + * A user how can view the status report and run readiness checkers. + * + * @var bool|\Drupal\user\Entity\User + */ + protected $checkerRunnerUser; + /** * {@inheritdoc} */ protected function setUp(): void { parent::setUp(); - $this->drupalLogin($this->createUser([ + $this->reportViewerUser = $this->createUser([ + 'administer site configuration', + ]); + $this->checkerRunnerUser = $this->createUser([ 'administer site configuration', - ])); + 'administer software updates', + ]); } /** @@ -33,49 +51,50 @@ protected function setUp(): void { */ public function testReadinessChecksStatusReport() { $assert = $this->assertSession(); + $this->container->get('module_installer')->uninstall(['automated_cron']); $this->container->get('module_installer')->install(['auto_updates', 'auto_updates_test']); - $this->drupalGet('admin/reports/status'); - $assert->pageTextContains('Update readiness checks'); - $assert->pageTextContains('Your site is ready to for automatic updates.'); - $assert->pageTextNotContains('Run readiness checks manually'); - $this->drupalGet('admin/config/auto_updates/readiness'); - $assert->pageTextContains("Access denied"); + + // If the the site is ready for updates the users will see the same output + // regardless of the user has permission to run updates. + foreach ([$this->reportViewerUser, $this->checkerRunnerUser] as $user) { + $this->drupalLogin($user); + $this->drupalGet('admin/reports/status'); + $this->assertReadinessReportMatches('Your site is ready for automatic updates.'); + } // Confirm a user without the permission to run readiness checks does not // have a link to run the checks when the checks need to run again. $this->setFakeTime('+2 days'); + $this->drupalLogin($this->reportViewerUser); $this->drupalGet('admin/reports/status'); - $assert->pageTextNotContains('Your site is ready to for automatic updates.'); - $assert->pageTextNotContains('Run readiness checks manually'); - $assert->pageTextContains('Your site has not recently checked if it is ready to apply automatic updates'); - $this->assertStringContainsString('Readiness checks were last run', $this->getReadinessReportText()); + $this->assertReadinessReportMatches('Your site has not recently checked if it is ready to apply automatic updates. Readiness checks were last run %s ago.'); // Confirm a user with the permission to run readiness checks does have a // link to run the checks when the checks need to run again. - $this->drupalLogin($this->createUser([ - 'administer site configuration', - 'administer software updates', - ])); + $this->drupalLogin($this->checkerRunnerUser); $this->drupalGet('admin/reports/status'); - $assert->pageTextContains('Update readiness checks'); - $assert->pageTextContains('Your site has not recently checked if it is ready to apply automatic updates'); - $this->assertStringContainsString('Last run', $this->getReadinessReportText()); - $assert->pageTextNotContains('Your site is ready to for automatic updates.'); - $this->container->get('state')->set(TestChecker::STATE_KEY, 'OMG 😱. Your site is not ready.'); + $this->assertReadinessReportMatches('Your site has not recently checked if it is ready to apply automatic updates.' + . ' Readiness checks were last run %s ago. Run readiness checks manually.'); + $this->container->get('state')->set(TestChecker::STATE_KEY, 'OMG 🚒. Your server is on 🔥!'); + + // Run the readiness checks. $this->clickLink('Run readiness checks'); - $assert->pageTextNotContains("Access denied"); - $assert->pageTextContains('Your site is currently failing readiness checks for automatic updates. It cannot be automatically updated until further action is performed:'); - $assert->pageTextContains('OMG 😱. Your site is not ready.'); // @todo If coming from the status report page should you be redirected there? // This is how 'Run cron" works. $assert->addressEquals('/admin/config/auto_updates'); - - // Confirm the error is displayed on the status report page. - $this->drupalGet('admin/reports/status'); - $assert->pageTextContains('Update readiness checks'); - $assert->pageTextContains('OMG 😱. Your site is not ready.'); - $this->assertStringContainsString('1 check failed', $this->getReadinessReportText()); + $assert->pageTextNotContains("Access denied"); + $assert->pageTextContains('Your site is currently failing readiness checks for automatic updates. It cannot be automatically updated until further action is performed:'); + $assert->pageTextContains('OMG 🚒. Your server is on 🔥!'); + + foreach ([$this->reportViewerUser, $this->checkerRunnerUser] as $user) { + $this->drupalLogin($user); + // Confirm the error is displayed on the status report page. + $this->drupalGet('admin/reports/status'); + $this->assertReadinessReportMatches('1 check failed: OMG 🚒. Your server is on 🔥!'); + // @todo Should always show when the checks were last run and a link to + // run when there is an error? + } } /** @@ -83,47 +102,35 @@ public function testReadinessChecksStatusReport() { */ public function testReadinessCheckAfterInstall() { $assert = $this->assertSession(); - $this->drupalLogin($this->createUser([ - 'administer site configuration', - 'administer software updates', - ])); + $this->drupalLogin($this->checkerRunnerUser); $this->drupalGet('admin/reports/status'); $assert->pageTextNotContains('Update readiness checks'); $this->container->get('module_installer')->install(['auto_updates']); $this->drupalGet('admin/reports/status'); - $assert->pageTextContains('Update readiness checks'); - $assert->pageTextContains('Your site is ready to for automatic updates.'); + $this->assertReadinessReportMatches('Your site is ready for automatic updates.'); - $this->container->get('state')->set(TestChecker::STATE_KEY, 'OMG 😱. Your site is not ready.'); + $this->container->get('state')->set(TestChecker::STATE_KEY, '😿Oh no! A hacker now owns your files!'); $this->container->get('module_installer')->install(['auto_updates_test']); $this->drupalGet('admin/reports/status'); - $assert->pageTextContains('Update readiness checks'); - $assert->pageTextContains('OMG 😱. Your site is not ready.'); - $assert->pageTextNotContains('Your site is ready to for automatic updates.'); + $this->assertReadinessReportMatches('1 check failed: 😿Oh no! A hacker now owns your files!'); - // Confirm that installing a module that does not provider a new checker - // does not run the checkers on install. - $this->container->get('state')->set(TestChecker::STATE_KEY, 'OMG 🙀. Your site is not ready for a DIFFERENT reason'); + // Confirm that installing a module that does not provide a new checker does + // not run the checkers on install. + $this->container->get('state')->set(TestChecker::STATE_KEY, 'Security has been compromised. "pass123" was a bad password!'); $this->container->get('module_installer')->install(['color']); $this->drupalGet('admin/reports/status'); - $assert->pageTextContains('Update readiness checks'); - $assert->pageTextNotContains('OMG 🙀. Your site is not read for a DIFFERENT reason'); - $assert->pageTextContains('OMG 😱. Your site is not ready.'); - $assert->pageTextNotContains('Your site is ready to for automatic updates.'); + $this->assertReadinessReportMatches('1 check failed: 😿Oh no! A hacker now owns your files!'); // Confirm the new message is displayed after running the checkers manually. $this->drupalGet('admin/config/auto_updates'); $this->clickLink('run the readiness checks'); - $assert->pageTextContains('OMG 🙀. Your site is not ready for a DIFFERENT reason'); - $assert->pageTextNotContains('OMG 😱. Your site is not ready.'); + $assert->pageTextContains('Security has been compromised. "pass123" was a bad password!'); + $assert->pageTextNotContains('😿Oh no! A hacker now owns your files!'); $this->drupalGet('admin/reports/status'); - $assert->pageTextContains('Update readiness checks'); - $assert->pageTextContains('OMG 🙀. Your site is not ready for a DIFFERENT reason'); - $assert->pageTextNotContains('OMG 😱. Your site is not ready.'); - $assert->pageTextNotContains('Your site is ready to for automatic updates.'); + $this->assertReadinessReportMatches('1 check failed: Security has been compromised. "pass123" was a bad password!'); } /** @@ -131,29 +138,31 @@ public function testReadinessCheckAfterInstall() { */ public function testCronRun() { $assert = $this->assertSession(); + $this->drupalLogin($this->reportViewerUser); $this->container->get('module_installer')->install(['auto_updates', 'auto_updates_test']); $this->drupalGet('admin/reports/status'); - $assert->pageTextContains('Update readiness checks'); - $assert->pageTextContains('Your site is ready to for automatic updates.'); + $this->assertReadinessReportMatches('Your site is ready for automatic updates.'); - $this->container->get('state')->set(TestChecker::STATE_KEY, 'OMG 😱. Your site is not ready.'); + $this->container->get('state')->set(TestChecker::STATE_KEY, 'OMG 🚒. Your server is on 🔥!'); // Tests that running cron within 1 hour of the checkers running will not // run them again. $this->setFakeTime('+30 minutes'); $this->clickLink('Run cron'); - $assert->pageTextContains('Update readiness checks'); - $assert->pageTextNotContains('OMG 😱. Your site is not ready.'); - $assert->pageTextContains('Your site is ready to for automatic updates.'); + $this->assertReadinessReportMatches('Your site is ready for automatic updates.'); // Tests that running cron after 1 hour of the checkers running will run // them again. $this->setFakeTime('+65 minutes'); $this->clickLink('Run cron'); - $assert->pageTextContains('Update readiness checks'); - $assert->pageTextContains('OMG 😱. Your site is not ready.'); - $assert->pageTextNotContains('Your site is ready to for automatic updates.'); + $this->assertReadinessReportMatches('1 check failed: OMG 🚒. Your server is on 🔥!'); + // Tests that running cron after 1 hour of the checkers running during cron + // will run them again. + $this->container->get('state')->set(TestChecker::STATE_KEY, 'OMG 💦. Now your server is filled with water!'); + $this->setFakeTime('+125 minutes'); + $this->clickLink('Run cron'); + $this->assertReadinessReportMatches('1 check failed: OMG 💦. Now your server is filled with water!'); } /** @@ -162,22 +171,25 @@ public function testCronRun() { * @param string $offset * A date/time offset string. */ - private function setFakeTime($offset): void { + private function setFakeTime(string $offset): void { $fake_delay = (new \DateTime())->modify($offset)->format(TestTime::TIME_FORMAT); $this->container->get('state')->set(TestTime::STATE_KEY, $fake_delay); } /** - * Gets the text on status report page of the readiness report item. + * Asserts status report readiness report item matches a format. * - * @return string - * The readiness checks status report text. + * @param string $format + * The string to match. */ - private function getReadinessReportText() { - return $this->getSession()->getPage()->find( + private function assertReadinessReportMatches(string $format): void { + // Prefix the expected format with the item title which does not change. + $format = "Update readiness checks $format"; + $text = $this->getSession()->getPage()->find( 'css', 'details.system-status-report__entry:contains("Update readiness checks")' )->getText(); + $this->assertStringMatchesFormat($format, $text); } }