From 83415837e5f690aca25a54efb27fbc14a8eddf08 Mon Sep 17 00:00:00 2001 From: Adam G-H <32250-phenaproxima@users.noreply.drupalcode.org> Date: Fri, 2 Jun 2023 18:28:22 +0000 Subject: [PATCH] Issue #3359727 by phenaproxima, tedbow: Add new setting for how unattended updates will be run --- automatic_updates.install | 2 +- automatic_updates.module | 5 +- config/install/automatic_updates.settings.yml | 4 +- config/schema/automatic_updates.schema.yml | 19 ++- src/Commands/AutomaticUpdatesCommands.php | 33 ++++- src/CronUpdateStage.php | 28 ++++- src/Validation/AdminStatusCheckMessages.php | 20 ++- src/Validation/StatusCheckRequirements.php | 46 ++++++- src/Validation/StatusChecker.php | 8 +- .../automatic_updates_test_cron.module | 4 +- tests/src/Build/UpdateTestBase.php | 3 +- .../AutomaticUpdatesFunctionalTestBase.php | 4 +- tests/src/Functional/StatusCheckTest.php | 117 +++++++++++++++--- .../Kernel/AutomaticUpdatesKernelTestBase.php | 14 ++- tests/src/Kernel/CronUpdateStageTest.php | 18 ++- tests/src/Kernel/HookCronTest.php | 36 ++++++ .../CronFrequencyValidatorTest.php | 2 +- .../StatusCheck/CronServerValidatorTest.php | 4 +- .../PhpExtensionsValidatorTest.php | 6 +- .../StatusCheckFailureEmailTest.php | 8 +- .../Kernel/StatusCheck/StatusCheckerTest.php | 2 +- .../VersionPolicyValidatorTest.php | 4 +- 22 files changed, 328 insertions(+), 59 deletions(-) create mode 100644 tests/src/Kernel/HookCronTest.php diff --git a/automatic_updates.install b/automatic_updates.install index 935915f697..2a0c7900c9 100644 --- a/automatic_updates.install +++ b/automatic_updates.install @@ -30,7 +30,7 @@ function automatic_updates_requirements($phase) { // Check that site has cron updates enabled or not. // @todo Remove in https://www.drupal.org/project/automatic_updates/issues/3284443 - if (\Drupal::configFactory()->get('automatic_updates.settings')->get('cron') !== CronUpdateStage::DISABLED) { + if (\Drupal::configFactory()->get('automatic_updates.settings')->get('unattended.level') !== CronUpdateStage::DISABLED) { $requirements['automatic_updates_cron'] = [ 'title' => t('Cron installs updates automatically'), 'severity' => SystemManager::REQUIREMENT_WARNING, diff --git a/automatic_updates.module b/automatic_updates.module index 9082ecd789..4e461d2195 100644 --- a/automatic_updates.module +++ b/automatic_updates.module @@ -165,7 +165,10 @@ function automatic_updates_module_implements_alter(&$implementations, $hook) { function automatic_updates_cron() { // @todo Refactor this after https://www.drupal.org/project/drupal/issues/2538292 // @todo Remove this after https://www.drupal.org/project/drupal/issues/3318964 - if (defined('MAINTENANCE_MODE') || stripos($_SERVER['PHP_SELF'], 'update.php') !== FALSE) { + // We don't want to run status checks if we're on the command line, because + // if unattended updates are configured to run via the web, running status + // checks on the command line could cause invalid results to get cached. + if (defined('MAINTENANCE_MODE') || stripos($_SERVER['PHP_SELF'], 'update.php') !== FALSE || CronUpdateStage::isCommandLine()) { return; } diff --git a/config/install/automatic_updates.settings.yml b/config/install/automatic_updates.settings.yml index 2649653a1e..cb20706e33 100644 --- a/config/install/automatic_updates.settings.yml +++ b/config/install/automatic_updates.settings.yml @@ -1,3 +1,5 @@ -cron: disable +unattended: + method: web + level: disable allow_core_minor_updates: false status_check_mail: errors_only diff --git a/config/schema/automatic_updates.schema.yml b/config/schema/automatic_updates.schema.yml index dddf10e819..9542782743 100644 --- a/config/schema/automatic_updates.schema.yml +++ b/config/schema/automatic_updates.schema.yml @@ -2,9 +2,22 @@ automatic_updates.settings: type: config_object label: 'Automatic Updates settings' mapping: - cron: - type: string - label: 'Enable automatic updates during cron' + unattended: + type: mapping + label: 'Settings for unattended update' + mapping: + method: + type: string + label: 'Method of running unattended updates' + constraints: + NotNull: [] + Choice: [console, web] + level: + type: string + label: 'Which level of unattended updates to perform' + constraints: + NotNull: [] + Choice: [disable, security, patch] cron_port: type: integer label: 'Port to use for finalization sub-request' diff --git a/src/Commands/AutomaticUpdatesCommands.php b/src/Commands/AutomaticUpdatesCommands.php index e15219919e..4646cde772 100644 --- a/src/Commands/AutomaticUpdatesCommands.php +++ b/src/Commands/AutomaticUpdatesCommands.php @@ -5,6 +5,9 @@ declare(strict_types = 1); namespace Drupal\automatic_updates\Commands; use Drupal\automatic_updates\DrushUpdateStage; +use Drupal\automatic_updates\StatusCheckMailer; +use Drupal\automatic_updates\Validation\StatusChecker; +use Drupal\Core\Config\ConfigFactoryInterface; use Drush\Commands\DrushCommands; /** @@ -22,8 +25,19 @@ final class AutomaticUpdatesCommands extends DrushCommands { * * @param \Drupal\automatic_updates\DrushUpdateStage $stage * The console cron updater service. + * @param \Drupal\automatic_updates\Validation\StatusChecker $statusChecker + * The status checker service. + * @param \Drupal\automatic_updates\StatusCheckMailer $statusCheckMailer + * The status check mailer service. + * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory + * The config factory service. */ - public function __construct(private readonly DrushUpdateStage $stage) { + public function __construct( + private readonly DrushUpdateStage $stage, + private readonly StatusChecker $statusChecker, + private readonly StatusCheckMailer $statusCheckMailer, + private readonly ConfigFactoryInterface $configFactory, + ) { parent::__construct(); } @@ -60,6 +74,7 @@ final class AutomaticUpdatesCommands extends DrushCommands { $io->info('Running post-apply tasks and final clean-up...'); $this->stage->handlePostApply($options['stage-id'], $options['from-version'], $options['to-version']); + $this->runStatusChecks(); } else { if ($this->stage->getMode() === DrushUpdateStage::DISABLED) { @@ -75,8 +90,24 @@ final class AutomaticUpdatesCommands extends DrushCommands { } else { $io->info("There is no Drupal core update available."); + $this->runStatusChecks(); } } } + /** + * Runs status checks, and sends failure notifications if necessary. + */ + private function runStatusChecks(): void { + $method = $this->configFactory->get('automatic_updates.settings') + ->get('unattended.method'); + + // To ensure consistent results, only run the status checks if we're + // explicitly configured to do unattended updates on the command line. + if ($method === 'console') { + $last_results = $this->statusChecker->getResults(); + $this->statusCheckMailer->sendFailureNotifications($last_results, $this->statusChecker->run()->getResults()); + } + } + } diff --git a/src/CronUpdateStage.php b/src/CronUpdateStage.php index 365322f6b3..533256f783 100644 --- a/src/CronUpdateStage.php +++ b/src/CronUpdateStage.php @@ -40,6 +40,13 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; */ class CronUpdateStage extends UpdateStage implements CronInterface { + /** + * The current interface between PHP and the server. + * + * @var string + */ + private static $serverApi = PHP_SAPI; + /** * All automatic updates are disabled. * @@ -121,6 +128,16 @@ class CronUpdateStage extends UpdateStage implements CronInterface { parent::__construct($composerInspector, $pathLocator, $beginner, $stager, $committer, $fileSystem, $eventDispatcher, $tempStoreFactory, $time, $pathFactory, $failureMarker); } + /** + * Indicates if we are currently running at the command line. + * + * @return bool + * TRUE if we are running at the command line, otherwise FALSE. + */ + final public static function isCommandLine(): bool { + return self::$serverApi === 'cli'; + } + /** * Handles updates during cron. * @@ -392,7 +409,7 @@ class CronUpdateStage extends UpdateStage implements CronInterface { * allowed during cron. */ final public function getMode(): string { - $mode = $this->configFactory->get('automatic_updates.settings')->get('cron'); + $mode = $this->configFactory->get('automatic_updates.settings')->get('unattended.level'); return $mode ?: static::SECURITY; } @@ -400,7 +417,14 @@ class CronUpdateStage extends UpdateStage implements CronInterface { * {@inheritdoc} */ public function run() { - if ($this->handleCron()) { + $method = $this->configFactory->get('automatic_updates.settings') + ->get('unattended.method'); + + // If we are configured to run updates via the web, and we're actually being + // accessed via the web (i.e., anything that isn't the command line), go + // ahead and try to do the update. In all other circumstances, just run the + // normal cron handler. + if ($method === 'web' && !self::isCommandLine() && $this->handleCron()) { return TRUE; } return $this->inner->run(); diff --git a/src/Validation/AdminStatusCheckMessages.php b/src/Validation/AdminStatusCheckMessages.php index acc27ca9ab..4e0d9df309 100644 --- a/src/Validation/AdminStatusCheckMessages.php +++ b/src/Validation/AdminStatusCheckMessages.php @@ -5,6 +5,7 @@ declare(strict_types = 1); namespace Drupal\automatic_updates\Validation; use Drupal\automatic_updates\CronUpdateStage; +use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Messenger\MessengerTrait; @@ -49,6 +50,8 @@ final class AdminStatusCheckMessages implements ContainerInjectionInterface { * The cron update stage service. * @param \Drupal\Core\Render\RendererInterface $renderer * The renderer service. + * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory + * The config factory service. */ public function __construct( private readonly StatusChecker $statusChecker, @@ -56,7 +59,8 @@ final class AdminStatusCheckMessages implements ContainerInjectionInterface { private readonly AccountProxyInterface $currentUser, private readonly CurrentRouteMatch $currentRouteMatch, private readonly CronUpdateStage $stage, - private readonly RendererInterface $renderer + private readonly RendererInterface $renderer, + private readonly ConfigFactoryInterface $configFactory ) {} /** @@ -69,7 +73,8 @@ final class AdminStatusCheckMessages implements ContainerInjectionInterface { $container->get('current_user'), $container->get('current_route_match'), $container->get('automatic_updates.cron_update_stage'), - $container->get('renderer') + $container->get('renderer'), + $container->get('config.factory') ); } @@ -81,12 +86,21 @@ final class AdminStatusCheckMessages implements ContainerInjectionInterface { return; } if ($this->statusChecker->getResults() === NULL) { + $method = $this->configFactory->get('automatic_updates.settings') + ->get('unattended.method'); + $checker_url = Url::fromRoute('automatic_updates.status_check')->setOption('query', $this->getDestinationArray()); - if ($checker_url->access()) { + if ($method === 'web' && $checker_url->access()) { $this->messenger()->addError($this->t('Your site has not recently run an update readiness check. <a href=":url">Rerun readiness checks now.</a>', [ ':url' => $checker_url->toString(), ])); } + elseif ($method === 'console') { + // @todo Link to the documentation on how to set up unattended updates + // via the terminal in https://drupal.org/i/3362695. + $message = $this->t('Unattended updates are configured to run via the console, but not appear to have run recently.'); + $this->messenger()->addError($message); + } } else { // Display errors, if there are any. If there aren't, then display diff --git a/src/Validation/StatusCheckRequirements.php b/src/Validation/StatusCheckRequirements.php index 573d859389..bc005d79ef 100644 --- a/src/Validation/StatusCheckRequirements.php +++ b/src/Validation/StatusCheckRequirements.php @@ -4,6 +4,7 @@ declare(strict_types = 1); namespace Drupal\automatic_updates\Validation; +use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Datetime\DateFormatterInterface; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; @@ -33,10 +34,13 @@ final class StatusCheckRequirements implements ContainerInjectionInterface { * The status checker service. * @param \Drupal\Core\Datetime\DateFormatterInterface $dateFormatter * The date formatter service. + * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory + * The config factory service. */ public function __construct( private readonly StatusChecker $statusChecker, - private readonly DateFormatterInterface $dateFormatter + private readonly DateFormatterInterface $dateFormatter, + private readonly ConfigFactoryInterface $configFactory, ) {} /** @@ -45,10 +49,23 @@ final class StatusCheckRequirements implements ContainerInjectionInterface { public static function create(ContainerInterface $container): self { return new static( $container->get('automatic_updates.status_checker'), - $container->get('date.formatter') + $container->get('date.formatter'), + $container->get('config.factory'), ); } + /** + * Returns the method used to run unattended updates. + * + * @return string + * The method used to run unattended updates. Will be either 'console' or + * 'web'. + */ + private function getMethod(): string { + return $this->configFactory->get('automatic_updates.settings') + ->get('unattended.method'); + } + /** * Gets requirements arrays as specified in hook_requirements(). * @@ -56,8 +73,26 @@ final class StatusCheckRequirements implements ContainerInjectionInterface { * Requirements arrays as specified by hook_requirements(). */ public function getRequirements(): array { - $results = $this->statusChecker->run()->getResults(); $requirements = []; + + $results = $this->statusChecker->getResults(); + // If unattended updates are run on the terminal, we don't want to do the + // status check right now, since running them over the web may yield + // inaccurate or irrelevant results. The console command runs status checks, + // so if there are no results, we can assume it has not been run in a while, + // and raise an error about that. + if (is_null($results) && $this->getMethod() === 'console') { + $requirements['automatic_updates_status_check_console_command_not_run'] = [ + 'title' => $this->t('Update readiness checks'), + 'severity' => SystemManager::REQUIREMENT_ERROR, + // @todo Link to the documentation on how to set up unattended updates + // via the terminal in https://drupal.org/i/3362695. + 'value' => $this->t('Unattended updates are configured to run via the console, but do not appear to have run recently.'), + ]; + return $requirements; + } + + $results ??= $this->statusChecker->run()->getResults(); if (empty($results)) { $requirements['automatic_updates_status_check'] = [ 'title' => $this->t('Update readiness checks'), @@ -144,6 +179,11 @@ final class StatusCheckRequirements implements ContainerInjectionInterface { * NULL. */ protected function createRunLink(): ?TranslatableMarkup { + // Only show this link if unattended updates are being run over the web. + if ($this->getMethod() !== 'web') { + return NULL; + } + $status_check_url = Url::fromRoute('automatic_updates.status_check'); if ($status_check_url->access()) { return $this->t( diff --git a/src/Validation/StatusChecker.php b/src/Validation/StatusChecker.php index 2a358b5bac..c69d463c38 100644 --- a/src/Validation/StatusChecker.php +++ b/src/Validation/StatusChecker.php @@ -154,13 +154,15 @@ final class StatusChecker implements EventSubscriberInterface { $this->clearStoredResults(); } elseif ($config->getName() === 'automatic_updates.settings') { + // If anything about how we run unattended updates has changed, clear the + // stored results, since they can be affected by these settings. + if ($event->isChanged('unattended')) { + $this->clearStoredResults(); + } // We only send status check failure notifications if unattended updates // are enabled. If notifications were previously disabled but have been // re-enabled, or their sensitivity level has changed, clear the stored // results so that we'll send accurate notifications next time cron runs. - if ($event->isChanged('cron') && $config->getOriginal('cron') === CronUpdateStage::DISABLED) { - $this->clearStoredResults(); - } elseif ($event->isChanged('status_check_mail') && $config->get('status_check_mail') !== StatusCheckMailer::DISABLED) { $this->clearStoredResults(); } diff --git a/tests/modules/automatic_updates_test_cron/automatic_updates_test_cron.module b/tests/modules/automatic_updates_test_cron/automatic_updates_test_cron.module index 2b7f3f485c..901db03a19 100644 --- a/tests/modules/automatic_updates_test_cron/automatic_updates_test_cron.module +++ b/tests/modules/automatic_updates_test_cron/automatic_updates_test_cron.module @@ -37,7 +37,7 @@ function automatic_updates_test_cron_form_update_settings_alter(array &$form, Fo CronUpdateStage::ALL => t('All supported updates'), CronUpdateStage::SECURITY => t('Security updates only'), ], - '#default_value' => \Drupal::config('automatic_updates.settings')->get('cron'), + '#default_value' => \Drupal::config('automatic_updates.settings')->get('unattended.level'), '#description' => t( 'If enabled, Drupal core will be automatically updated when an update is available. Automatic updates are only supported for @current_minor.x versions of Drupal core. Drupal @current_minor will receive security updates until @supported_until_version is released.', [ @@ -58,6 +58,6 @@ function automatic_updates_test_cron_form_update_settings_alter(array &$form, Fo function _automatic_updates_test_cron_update_settings_form_submit(array &$form, FormStateInterface $form_state) { \Drupal::configFactory() ->getEditable('automatic_updates.settings') - ->set('cron', $form_state->getValue('automatic_updates_cron')) + ->set('unattended.level', $form_state->getValue('automatic_updates_cron')) ->save(); } diff --git a/tests/src/Build/UpdateTestBase.php b/tests/src/Build/UpdateTestBase.php index 6ff090d657..c507716688 100644 --- a/tests/src/Build/UpdateTestBase.php +++ b/tests/src/Build/UpdateTestBase.php @@ -21,7 +21,7 @@ abstract class UpdateTestBase extends TemplateProjectTestBase { parent::createTestProject($template); // @todo Remove in https://www.drupal.org/project/automatic_updates/issues/3284443 $code = <<<END -\$config['automatic_updates.settings']['cron'] = 'security'; +\$config['automatic_updates.settings']['unattended']['level'] = 'security'; END; $this->writeSettings($code); // Install Automatic Updates, and other modules needed for testing. @@ -69,6 +69,7 @@ END; $this->visit('/admin/reports/status'); $mink = $this->getMink(); $page = $mink->getSession()->getPage(); + $page->clickLink('Rerun readiness checks'); $readiness_check_summaries = $page->findAll('css', 'summary:contains("Update readiness checks")'); // There should always either be the summary section indicating the site is diff --git a/tests/src/Functional/AutomaticUpdatesFunctionalTestBase.php b/tests/src/Functional/AutomaticUpdatesFunctionalTestBase.php index 62b8461162..f518f16a8d 100644 --- a/tests/src/Functional/AutomaticUpdatesFunctionalTestBase.php +++ b/tests/src/Functional/AutomaticUpdatesFunctionalTestBase.php @@ -39,7 +39,9 @@ abstract class AutomaticUpdatesFunctionalTestBase extends BrowserTestBase { parent::setUp(); $this->useFixtureDirectoryAsActive(__DIR__ . '/../../../package_manager/tests/fixtures/fake_site'); // @todo Remove in https://www.drupal.org/project/automatic_updates/issues/3284443 - $this->config('automatic_updates.settings')->set('cron', CronUpdateStage::SECURITY)->save(); + $this->config('automatic_updates.settings') + ->set('unattended.level', CronUpdateStage::SECURITY) + ->save(); } /** diff --git a/tests/src/Functional/StatusCheckTest.php b/tests/src/Functional/StatusCheckTest.php index 3f9fa4505a..12aa80d0bc 100644 --- a/tests/src/Functional/StatusCheckTest.php +++ b/tests/src/Functional/StatusCheckTest.php @@ -127,6 +127,7 @@ class StatusCheckTest extends AutomaticUpdatesFunctionalTestBase { */ public function testStatusChecksOnStatusReport(): void { $assert = $this->assertSession(); + $page = $this->getSession()->getPage(); // Ensure automated_cron is disabled before installing automatic_updates. // This ensures we are testing that automatic_updates runs the checkers when @@ -186,49 +187,50 @@ class StatusCheckTest extends AutomaticUpdatesFunctionalTestBase { $this->drupalGet('admin/reports/status'); $this->assertErrors($expected_results); + $this->drupalLogin($this->checkerRunnerUser); + $this->drupalGet('/admin/reports/status'); + $expected_results = [ 'error' => $this->createValidationResult(SystemManager::REQUIREMENT_ERROR), 'warning' => $this->createValidationResult(SystemManager::REQUIREMENT_WARNING), ]; TestSubscriber1::setTestResult($expected_results, StatusCheckEvent::class); - // Confirm a new message is displayed if the page is reloaded. - $this->getSession()->reload(); - // On the status page, we should see the summaries and messages, even if - // there is only 1 message. - $this->assertErrors([$expected_results['error']]); - $this->assertWarnings([$expected_results['warning']]); + $page->clickLink('Rerun readiness checks'); + // We should see the summaries and messages, even if there's only 1 message. + $this->assertErrors([$expected_results['error']], TRUE); + $this->assertWarnings([$expected_results['warning']], TRUE); // If there's a result with only one message, but no summary, ensure that // message is displayed. $result = ValidationResult::createError([t('A lone message, with no summary.')]); TestSubscriber1::setTestResult([$result], StatusCheckEvent::class); - $this->getSession()->reload(); - $this->assertErrors([$result]); + $page->clickLink('Rerun readiness checks'); + $this->assertErrors([$result], TRUE); $expected_results = [ 'error' => $this->createValidationResult(SystemManager::REQUIREMENT_ERROR, 2), 'warning' => $this->createValidationResult(SystemManager::REQUIREMENT_WARNING, 2), ]; TestSubscriber1::setTestResult($expected_results, StatusCheckEvent::class); - $this->getSession()->reload(); - // Confirm that both messages and summaries will be displayed on status - // report when there multiple messages. - $this->assertErrors([$expected_results['error']]); - $this->assertWarnings([$expected_results['warning']]); + $page->clickLink('Rerun readiness checks'); + // Confirm that both messages and summaries will be displayed when there are + // multiple messages. + $this->assertErrors([$expected_results['error']], TRUE); + $this->assertWarnings([$expected_results['warning']], TRUE); $expected_results = [$this->createValidationResult(SystemManager::REQUIREMENT_WARNING, 2)]; TestSubscriber1::setTestResult($expected_results, StatusCheckEvent::class); - $this->getSession()->reload(); + $page->clickLink('Rerun readiness checks'); $assert->pageTextContainsOnce('Update readiness checks'); // Confirm that warnings will display on the status report if there are no // errors. - $this->assertWarnings($expected_results); + $this->assertWarnings($expected_results, TRUE); $expected_results = [$this->createValidationResult(SystemManager::REQUIREMENT_WARNING)]; TestSubscriber1::setTestResult($expected_results, StatusCheckEvent::class); - $this->getSession()->reload(); + $page->clickLink('Rerun readiness checks'); $assert->pageTextContainsOnce('Update readiness checks'); - $this->assertWarnings($expected_results); + $this->assertWarnings($expected_results, TRUE); } /** @@ -381,7 +383,9 @@ class StatusCheckTest extends AutomaticUpdatesFunctionalTestBase { // Confirm status check messages are not displayed when cron updates are // disabled. - $this->config('automatic_updates.settings')->set('cron', CronUpdateStage::DISABLED)->save(); + $this->config('automatic_updates.settings') + ->set('unattended.level', CronUpdateStage::DISABLED) + ->save(); $this->drupalGet('admin/structure'); $this->checkForMetaRefresh(); $assert->pageTextNotContains(static::$warningsExplanation); @@ -403,7 +407,9 @@ class StatusCheckTest extends AutomaticUpdatesFunctionalTestBase { // the functionality to retrieve our fake release history metadata. $this->container->get('module_installer')->install(['automatic_updates', 'automatic_updates_test']); // @todo Remove in https://www.drupal.org/project/automatic_updates/issues/3284443 - $this->config('automatic_updates.settings')->set('cron', CronUpdateStage::SECURITY)->save(); + $this->config('automatic_updates.settings') + ->set('unattended.level', CronUpdateStage::SECURITY) + ->save(); $this->drupalGet('admin/reports/status'); $this->assertNoErrors(TRUE); @@ -578,6 +584,79 @@ class StatusCheckTest extends AutomaticUpdatesFunctionalTestBase { $assert_session->pageTextContains($no_results_message); } + /** + * Tests that the status report shows cached status check results. + */ + public function testStatusReportShowsCachedResults(): void { + $session = $this->getSession(); + $this->drupalLogin($this->checkerRunnerUser); + + $this->container->get('module_installer')->install([ + 'automatic_updates', + 'automatic_updates_test', + ]); + $this->container = $this->container->get('kernel')->getContainer(); + + // Clear stored results that were collected when the module was installed. + $this->container->get('automatic_updates.status_checker') + ->clearStoredResults(); + + // Flag a validation error, whose summary will be displayed in the messages + // area. + $result = $this->createValidationResult(SystemManager::REQUIREMENT_ERROR); + TestSubscriber1::setTestResult([$result], StatusCheckEvent::class); + + $this->drupalGet('/admin/reports/status'); + $this->assertErrors([$result], TRUE); + + // Clear the result, and ensure that it's still visible because it is + // cached. + TestSubscriber::setTestResult(NULL, StatusCheckEvent::class); + $session->reload(); + $this->assertErrors([$result], TRUE); + + // If unattended updates are configured to run via the command line, we + // should see a warning that the status checks have not run recently. This + // is because changing the configuration clears the cached results, since + // they may be affected by the change. + // @see \Drupal\automatic_updates\Validation\StatusChecker::onConfigSave() + $this->config('automatic_updates.settings') + ->set('unattended.method', 'console') + ->save(); + $session->reload(); + $assert_session = $this->assertSession(); + $assert_session->pageTextContainsOnce('Unattended updates are configured to run via the console, but do not appear to have run recently.'); + $assert_session->pageTextNotContains((string) $result->messages[0]); + } + + /** + * Tests the status checks when unattended updates are run via the console. + */ + public function testUnattendedUpdatesRunFromConsole(): void { + $this->container->get('module_installer')->install(['automatic_updates']); + $this->container = $this->container->get('kernel')->getContainer(); + + // Clear stored results that were collected when the module was installed. + $this->container->get('automatic_updates.status_checker') + ->clearStoredResults(); + + $this->config('automatic_updates.settings') + ->set('unattended.method', 'console') + ->save(); + + // If we visit the status report, we should see an error requirement because + // unattended updates are configured to run via the terminal, and there are + // no stored status check results, which means that the console command has + // probably not run recently (or ever). + $this->drupalGet('/admin/reports/status'); + $this->assertRequirement('error', 'Unattended updates are configured to run via the console, but do not appear to have run recently.', [], FALSE); + + // We should see a similar message on any other admin page. + $this->drupalGet('/admin/structure'); + $this->assertSession() + ->statusMessageContains('Unattended updates are configured to run via the console, but not appear to have run recently.', 'error'); + } + /** * Asserts that the status check requirement displays no errors or warnings. * diff --git a/tests/src/Kernel/AutomaticUpdatesKernelTestBase.php b/tests/src/Kernel/AutomaticUpdatesKernelTestBase.php index 9ac2612e13..c4765f78e2 100644 --- a/tests/src/Kernel/AutomaticUpdatesKernelTestBase.php +++ b/tests/src/Kernel/AutomaticUpdatesKernelTestBase.php @@ -51,7 +51,12 @@ abstract class AutomaticUpdatesKernelTestBase extends PackageManagerKernelTestBa parent::setUp(); // Enable cron updates, which will eventually be the default. // @todo Remove in https://www.drupal.org/project/automatic_updates/issues/3284443 - $this->config('automatic_updates.settings')->set('cron', CronUpdateStage::SECURITY)->save(); + $this->config('automatic_updates.settings') + ->set('unattended', [ + 'method' => 'web', + 'level' => CronUpdateStage::SECURITY, + ]) + ->save(); // By default, pretend we're running Drupal core 9.8.0 and a non-security // update to 9.8.1 is available. @@ -62,6 +67,13 @@ abstract class AutomaticUpdatesKernelTestBase extends PackageManagerKernelTestBa // from a sane state. // @see \Drupal\automatic_updates\Validator\CronFrequencyValidator $this->container->get('state')->set('system.cron_last', time()); + + // Cron updates are not done when running at the command line, so override + // our cron handler's PHP_SAPI constant to a valid value that isn't `cli`. + // The choice of `cgi-fcgi` is arbitrary; see + // https://www.php.net/php_sapi_name for some valid values of PHP_SAPI. + $property = new \ReflectionProperty(CronUpdateStage::class, 'serverApi'); + $property->setValue(NULL, 'cgi-fcgi'); } /** diff --git a/tests/src/Kernel/CronUpdateStageTest.php b/tests/src/Kernel/CronUpdateStageTest.php index 7b2d32f4f1..b13e2d0ee3 100644 --- a/tests/src/Kernel/CronUpdateStageTest.php +++ b/tests/src/Kernel/CronUpdateStageTest.php @@ -156,7 +156,9 @@ class CronUpdateStageTest extends AutomaticUpdatesKernelTestBase { $this->setReleaseMetadata($release_data); $this->setCoreVersion('9.8.0'); update_get_available(TRUE); - $this->config('automatic_updates.settings')->set('cron', $setting)->save(); + $this->config('automatic_updates.settings') + ->set('unattended.level', $setting) + ->save(); // Since we're just trying to ensure that all of Package Manager's services // are called as expected, disable validation by replacing the event @@ -260,7 +262,9 @@ class CronUpdateStageTest extends AutomaticUpdatesKernelTestBase { } $this->installConfig('automatic_updates'); // @todo Remove in https://www.drupal.org/project/automatic_updates/issues/3284443 - $this->config('automatic_updates.settings')->set('cron', CronUpdateStage::SECURITY)->save(); + $this->config('automatic_updates.settings') + ->set('unattended.level', CronUpdateStage::SECURITY) + ->save(); // Ensure that there is a security release to which we should update. $this->setReleaseMetadata([ 'drupal' => __DIR__ . "/../../../package_manager/tests/fixtures/release-history/drupal.9.8.1-security.xml", @@ -368,7 +372,9 @@ class CronUpdateStageTest extends AutomaticUpdatesKernelTestBase { * Tests stage is not destroyed if another update is applying. */ public function testStageNotDestroyedIfApplying(): void { - $this->config('automatic_updates.settings')->set('cron', CronUpdateStage::ALL)->save(); + $this->config('automatic_updates.settings') + ->set('unattended.level', CronUpdateStage::ALL) + ->save(); $this->setReleaseMetadata([ 'drupal' => __DIR__ . "/../../../package_manager/tests/fixtures/release-history/drupal.9.8.1-security.xml", ]); @@ -406,7 +412,9 @@ class CronUpdateStageTest extends AutomaticUpdatesKernelTestBase { * Tests stage is not destroyed if not available and site is on secure version. */ public function testStageNotDestroyedIfSecure(): void { - $this->config('automatic_updates.settings')->set('cron', CronUpdateStage::ALL)->save(); + $this->config('automatic_updates.settings') + ->set('unattended.level', CronUpdateStage::ALL) + ->save(); $this->setReleaseMetadata([ 'drupal' => __DIR__ . "/../../../package_manager/tests/fixtures/release-history/drupal.9.8.2.xml", ]); @@ -548,7 +556,7 @@ END; 'drupal' => __DIR__ . '/../../../package_manager/tests/fixtures/release-history/drupal.9.8.2.xml', ]); $this->config('automatic_updates.settings') - ->set('cron', CronUpdateStage::ALL) + ->set('unattended.level', CronUpdateStage::ALL) ->save(); $error = ValidationResult::createError([ diff --git a/tests/src/Kernel/HookCronTest.php b/tests/src/Kernel/HookCronTest.php new file mode 100644 index 0000000000..b4b2e845f2 --- /dev/null +++ b/tests/src/Kernel/HookCronTest.php @@ -0,0 +1,36 @@ +<?php + +namespace Drupal\Tests\automatic_updates\Kernel; + +use Drupal\automatic_updates\Validation\StatusChecker; + +/** + * @group automatic_updates + */ +class HookCronTest extends AutomaticUpdatesKernelTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = ['automatic_updates']; + + /** + * Tests that our cron hook does not run if we're at the command line. + */ + public function testCronHookBypassedAtCommandLine(): void { + if (PHP_SAPI !== 'cli') { + $this->markTestSkipped('This test requires that PHP be running at the command line.'); + } + + // The status check should not have run yet. + /** @var \Drupal\automatic_updates\Validation\StatusChecker $status_checker */ + $status_checker = $this->container->get(StatusChecker::class); + $this->assertNull($status_checker->getLastRunTime()); + + // Since we're at the command line, status checks should still not run, even + // if we do run cron. + $this->container->get('cron')->run(); + $this->assertNull($status_checker->getResults()); + } + +} diff --git a/tests/src/Kernel/StatusCheck/CronFrequencyValidatorTest.php b/tests/src/Kernel/StatusCheck/CronFrequencyValidatorTest.php index c601479274..bbd2e7dfab 100644 --- a/tests/src/Kernel/StatusCheck/CronFrequencyValidatorTest.php +++ b/tests/src/Kernel/StatusCheck/CronFrequencyValidatorTest.php @@ -41,7 +41,7 @@ class CronFrequencyValidatorTest extends AutomaticUpdatesKernelTestBase { */ public function testNoValidationIfCronDisabled(): void { $this->config('automatic_updates.settings') - ->set('cron', CronUpdateStage::DISABLED) + ->set('unattended.level', CronUpdateStage::DISABLED) ->save(); $validator = new class ( diff --git a/tests/src/Kernel/StatusCheck/CronServerValidatorTest.php b/tests/src/Kernel/StatusCheck/CronServerValidatorTest.php index 0c036ee256..9c7fa9026c 100644 --- a/tests/src/Kernel/StatusCheck/CronServerValidatorTest.php +++ b/tests/src/Kernel/StatusCheck/CronServerValidatorTest.php @@ -111,7 +111,7 @@ class CronServerValidatorTest extends AutomaticUpdatesKernelTestBase { $property->setValue(NULL, $server_api); $this->config('automatic_updates.settings') - ->set('cron', $cron_mode) + ->set('unattended.level', $cron_mode) ->set('cron_port', $alternate_port ? 2501 : 0) ->save(); @@ -169,7 +169,7 @@ class CronServerValidatorTest extends AutomaticUpdatesKernelTestBase { ->addLogger($logger); $this->config('automatic_updates.settings') - ->set('cron', $cron_mode) + ->set('unattended.level', $cron_mode) ->save(); // Add a listener to change the $server_api and $alternate_port settings diff --git a/tests/src/Kernel/StatusCheck/PhpExtensionsValidatorTest.php b/tests/src/Kernel/StatusCheck/PhpExtensionsValidatorTest.php index edcbbc3790..c8917b7a36 100644 --- a/tests/src/Kernel/StatusCheck/PhpExtensionsValidatorTest.php +++ b/tests/src/Kernel/StatusCheck/PhpExtensionsValidatorTest.php @@ -44,12 +44,12 @@ class PhpExtensionsValidatorTest extends AutomaticUpdatesKernelTestBase { // If unattended updates are disabled, we should only see a warning from // Package Manager. - $config->set('cron', CronUpdateStage::DISABLED)->save(); + $config->set('unattended.level', CronUpdateStage::DISABLED)->save(); $this->assertCheckerResultsFromManager([$warning_result], TRUE); // The parent class' setUp() method simulates an available security update, // so ensure that the cron update stage will try to update to it. - $config->set('cron', CronUpdateStage::SECURITY)->save(); + $config->set('unattended.level', CronUpdateStage::SECURITY)->save(); // If unattended updates are enabled, we should see an error from Automatic // Updates. @@ -77,7 +77,7 @@ class PhpExtensionsValidatorTest extends AutomaticUpdatesKernelTestBase { // The parent class' setUp() method simulates an available security // update, so ensure that the cron update stage will try to update to it. $this->config('automatic_updates.settings') - ->set('cron', CronUpdateStage::SECURITY) + ->set('unattended.level', CronUpdateStage::SECURITY) ->save(); $logger = new TestLogger(); diff --git a/tests/src/Kernel/StatusCheck/StatusCheckFailureEmailTest.php b/tests/src/Kernel/StatusCheck/StatusCheckFailureEmailTest.php index 4b143af32d..25ff4cdeaa 100644 --- a/tests/src/Kernel/StatusCheck/StatusCheckFailureEmailTest.php +++ b/tests/src/Kernel/StatusCheck/StatusCheckFailureEmailTest.php @@ -54,7 +54,9 @@ class StatusCheckFailureEmailTest extends AutomaticUpdatesKernelTestBase { $this->installConfig('automatic_updates'); // @todo Remove in https://www.drupal.org/project/automatic_updates/issues/3284443 - $this->config('automatic_updates.settings')->set('cron', CronUpdateStage::SECURITY)->save(); + $this->config('automatic_updates.settings') + ->set('unattended.level', CronUpdateStage::SECURITY) + ->save(); $this->setUpEmailRecipients(); // Allow stored available update data to live for a very, very long time. @@ -212,7 +214,7 @@ END; // If we disable unattended updates entirely and flag a new error, they // should not be e-mailed. - $config->set('cron', CronUpdateStage::DISABLED)->save(); + $config->set('unattended.level', CronUpdateStage::DISABLED)->save(); $error = $this->createValidationResult(SystemManager::REQUIREMENT_ERROR); TestSubscriber1::setTestResult([$error], StatusCheckEvent::class); $this->runCron(); @@ -220,7 +222,7 @@ END; // If we re-enable unattended updates, they should be emailed again, even if // the results haven't changed. - $config->set('cron', CronUpdateStage::ALL)->save(); + $config->set('unattended.level', CronUpdateStage::ALL)->save(); $this->runCron(); $sent_messages_count += $recipient_count; $this->assertSentMessagesCount($sent_messages_count); diff --git a/tests/src/Kernel/StatusCheck/StatusCheckerTest.php b/tests/src/Kernel/StatusCheck/StatusCheckerTest.php index 0837b7108b..8ebddb72ae 100644 --- a/tests/src/Kernel/StatusCheck/StatusCheckerTest.php +++ b/tests/src/Kernel/StatusCheck/StatusCheckerTest.php @@ -212,7 +212,7 @@ class StatusCheckerTest extends AutomaticUpdatesKernelTestBase { // By default, updates will be enabled on cron. $this->assertInstanceOf(CronUpdateStage::class, $stage); $this->config('automatic_updates.settings') - ->set('cron', CronUpdateStage::DISABLED) + ->set('unattended.level', CronUpdateStage::DISABLED) ->save(); $this->container->get(StatusChecker::class)->run(); $this->assertInstanceOf(UpdateStage::class, $stage); diff --git a/tests/src/Kernel/StatusCheck/VersionPolicyValidatorTest.php b/tests/src/Kernel/StatusCheck/VersionPolicyValidatorTest.php index c9c8c2e0bc..a8e64641e8 100644 --- a/tests/src/Kernel/StatusCheck/VersionPolicyValidatorTest.php +++ b/tests/src/Kernel/StatusCheck/VersionPolicyValidatorTest.php @@ -198,7 +198,7 @@ class VersionPolicyValidatorTest extends AutomaticUpdatesKernelTestBase { foreach ($cron_modes as $cron_mode) { $this->config('automatic_updates.settings') - ->set('cron', $cron_mode) + ->set('unattended.level', $cron_mode) ->set('allow_core_minor_updates', $allow_minor_updates) ->save(); @@ -420,7 +420,7 @@ class VersionPolicyValidatorTest extends AutomaticUpdatesKernelTestBase { foreach ($cron_modes as $cron_mode) { $this->config('automatic_updates.settings') - ->set('cron', $cron_mode) + ->set('unattended.level', $cron_mode) ->set('allow_core_minor_updates', $allow_minor_updates) ->save(); -- GitLab