From f3c421e7eabb8e873a473d07363322b0835cb718 Mon Sep 17 00:00:00 2001 From: "kunal.sachdev" <kunal.sachdev@3685163.no-reply.drupal.org> Date: Fri, 4 Mar 2022 17:36:08 +0000 Subject: [PATCH] Issue #3248929 by kunal.sachdev: List update that will be applied on the UpdateReady form --- .../package_manager_test_fixture.info.yml | 6 ++ .../package_manager_test_fixture.services.yml | 8 ++ .../src/EventSubscriber/FixtureStager.php | 88 +++++++++++++++++++ src/Form/UpdateReady.php | 65 +++++++++----- tests/fixtures/staged/9.8.1/composer.json | 7 ++ .../9.8.1/vendor/composer/installed.json | 9 ++ .../AutomaticUpdatesFunctionalTestBase.php | 23 ++++- .../Functional/ReadinessValidationTest.php | 5 +- tests/src/Functional/UpdateLockTest.php | 6 +- tests/src/Functional/UpdaterFormTest.php | 53 +++++++++-- 10 files changed, 238 insertions(+), 32 deletions(-) create mode 100644 package_manager/tests/modules/package_manager_test_fixture/package_manager_test_fixture.info.yml create mode 100644 package_manager/tests/modules/package_manager_test_fixture/package_manager_test_fixture.services.yml create mode 100644 package_manager/tests/modules/package_manager_test_fixture/src/EventSubscriber/FixtureStager.php create mode 100644 tests/fixtures/staged/9.8.1/composer.json create mode 100644 tests/fixtures/staged/9.8.1/vendor/composer/installed.json diff --git a/package_manager/tests/modules/package_manager_test_fixture/package_manager_test_fixture.info.yml b/package_manager/tests/modules/package_manager_test_fixture/package_manager_test_fixture.info.yml new file mode 100644 index 0000000000..aaea8bb04c --- /dev/null +++ b/package_manager/tests/modules/package_manager_test_fixture/package_manager_test_fixture.info.yml @@ -0,0 +1,6 @@ +name: 'Package Manager Test Fixture' +description: 'Provides a mechanism for functional tests to stage fixture files.' +type: module +package: Testing +dependencies: + - automatic_updates:package_manager diff --git a/package_manager/tests/modules/package_manager_test_fixture/package_manager_test_fixture.services.yml b/package_manager/tests/modules/package_manager_test_fixture/package_manager_test_fixture.services.yml new file mode 100644 index 0000000000..aa44e5741e --- /dev/null +++ b/package_manager/tests/modules/package_manager_test_fixture/package_manager_test_fixture.services.yml @@ -0,0 +1,8 @@ +services: + package_manager_test_fixture.stager: + class: Drupal\package_manager_test_fixture\EventSubscriber\FixtureStager + arguments: + - '@state' + - '@package_manager.symfony_file_system' + tags: + - { name: event_subscriber } diff --git a/package_manager/tests/modules/package_manager_test_fixture/src/EventSubscriber/FixtureStager.php b/package_manager/tests/modules/package_manager_test_fixture/src/EventSubscriber/FixtureStager.php new file mode 100644 index 0000000000..11f8552ac6 --- /dev/null +++ b/package_manager/tests/modules/package_manager_test_fixture/src/EventSubscriber/FixtureStager.php @@ -0,0 +1,88 @@ +<?php + +namespace Drupal\package_manager_test_fixture\EventSubscriber; + +use Drupal\Core\State\StateInterface; +use Drupal\package_manager\Event\PostRequireEvent; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Filesystem\Filesystem; + +/** + * Defines an event subscriber which copies certain files into the staging area. + * + * This is most useful in conjunction with package_manager_bypass, which quietly + * turns all Composer Stager operations into no-ops. In such cases, no staging + * area will be physically created, but if a test needs to simulate certain + * conditions in a staging area without actually staging the active code base, + * this event subscriber is the way to do it. + */ +class FixtureStager implements EventSubscriberInterface { + + /** + * The state service. + * + * @var \Drupal\Core\State\StateInterface + */ + protected $state; + + /** + * The Symfony file system service. + * + * @var \Symfony\Component\Filesystem\Filesystem + */ + protected $fileSystem; + + /** + * Constructs a FixtureStager. + * + * @param \Drupal\Core\State\StateInterface $state + * The state service. + * @param \Symfony\Component\Filesystem\Filesystem $file_system + * The Symfony file system service. + */ + public function __construct(StateInterface $state, Filesystem $file_system) { + $this->state = $state; + $this->fileSystem = $file_system; + } + + /** + * Copies files from a fixture into the staging area. + * + * Tests which use this functionality are responsible for cleaning up the + * staging area. + * + * @param \Drupal\package_manager\Event\PostRequireEvent $event + * The event object. + * + * @see \Drupal\Tests\automatic_updates\Functional\AutomaticUpdatesFunctionalTestBase::tearDown() + */ + public function copyFilesFromFixture(PostRequireEvent $event): void { + $fixturePath = $this->state->get(static::class); + if ($fixturePath && is_dir($fixturePath)) { + $this->fileSystem->mirror($fixturePath, $event->getStage()->getStageDirectory(), NULL, [ + 'override' => TRUE, + 'delete' => TRUE, + ]); + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + return [ + PostRequireEvent::class => 'copyFilesFromFixture', + ]; + } + + /** + * Sets the path of the fixture to copy into the staging area. + * + * @param string $path + * The path of the fixture to copy into the staging area. + */ + public static function setFixturePath(string $path): void { + \Drupal::state()->set(static::class, $path); + } + +} diff --git a/src/Form/UpdateReady.php b/src/Form/UpdateReady.php index 5c15518cdc..ae2759d563 100644 --- a/src/Form/UpdateReady.php +++ b/src/Form/UpdateReady.php @@ -105,46 +105,71 @@ class UpdateReady extends FormBase { return $form; } - // Don't check for pending database updates if the form has been submitted, - // because we don't want to store the warning in the messenger during form - // submit. + $messages = []; + + // If there are any installed modules with database updates in the staging + // area, warn the user that they might be sent to update.php once the + // staged changes have been applied. + $pending_updates = $this->getModulesWithStagedDatabaseUpdates(); + if ($pending_updates) { + $messages[MessengerInterface::TYPE_WARNING][] = $this->t('Possible database updates were detected in the following modules; you may be redirected to the database update page in order to complete the update process.'); + foreach ($pending_updates as $info) { + $messages[MessengerInterface::TYPE_WARNING][] = $info['name']; + } + } + + try { + $staged_core_packages = $this->updater->getStageComposer() + ->getCorePackages(); + } + catch (\Throwable $exception) { + $messages[MessengerInterface::TYPE_ERROR][] = $this->t('There was an error loading the pending update. Press the <em>Cancel update</em> button to start over.'); + } + + // Don't set any messages if the form has been submitted, because we don't + // want them to be set during form submit. if (!$form_state->getUserInput()) { - // If there are any installed modules with database updates in the staging - // area, warn the user that they might be sent to update.php once the - // staged changes have been applied. - $pending_updates = $this->getModulesWithStagedDatabaseUpdates(); - - if ($pending_updates) { - $this->messenger()->addWarning($this->t('Possible database updates were detected in the following modules; you may be redirected to the database update page in order to complete the update process.')); - foreach ($pending_updates as $info) { - $this->messenger()->addWarning($info['name']); + foreach ($messages as $type => $messages_of_type) { + foreach ($messages_of_type as $message) { + $this->messenger()->addMessage($message, $type); } } } + $form['actions'] = [ + 'cancel' => [ + '#type' => 'submit', + '#value' => $this->t('Cancel update'), + '#submit' => ['::cancel'], + ], + '#type' => 'actions', + ]; $form['stage_id'] = [ '#type' => 'value', '#value' => $stage_id, ]; + if (empty($staged_core_packages)) { + return $form; + } + + $form['update_version'] = [ + '#type' => 'html_tag', + '#tag' => 'p', + '#value' => $this->t('Drupal core will be updated to %version', [ + '%version' => reset($staged_core_packages)->getPrettyVersion(), + ]), + ]; $form['backup'] = [ '#prefix' => '<strong>', '#markup' => $this->t('Back up your database and site before you continue. <a href=":backup_url">Learn how</a>.', [':backup_url' => 'https://www.drupal.org/node/22281']), '#suffix' => '</strong>', ]; - $form['maintenance_mode'] = [ '#title' => $this->t('Perform updates with site in maintenance mode (strongly recommended)'), '#type' => 'checkbox', '#default_value' => TRUE, ]; - - $form['actions'] = ['#type' => 'actions']; - $form['actions']['cancel'] = [ - '#type' => 'submit', - '#value' => $this->t('Cancel update'), - '#submit' => ['::cancel'], - ]; $form['actions']['submit'] = [ '#type' => 'submit', '#value' => $this->t('Continue'), diff --git a/tests/fixtures/staged/9.8.1/composer.json b/tests/fixtures/staged/9.8.1/composer.json new file mode 100644 index 0000000000..6a9ed719d0 --- /dev/null +++ b/tests/fixtures/staged/9.8.1/composer.json @@ -0,0 +1,7 @@ +{ + "extra": { + "_readme": [ + "This fixture simulates a staging area in which, according to Composer, Drupal core 9.8.1 is installed." + ] + } +} diff --git a/tests/fixtures/staged/9.8.1/vendor/composer/installed.json b/tests/fixtures/staged/9.8.1/vendor/composer/installed.json new file mode 100644 index 0000000000..87b5a18421 --- /dev/null +++ b/tests/fixtures/staged/9.8.1/vendor/composer/installed.json @@ -0,0 +1,9 @@ +{ + "packages": [ + { + "name": "drupal/core", + "version": "9.8.1", + "type": "drupal-core" + } + ] +} diff --git a/tests/src/Functional/AutomaticUpdatesFunctionalTestBase.php b/tests/src/Functional/AutomaticUpdatesFunctionalTestBase.php index 334ed3bb20..fa135b3adf 100644 --- a/tests/src/Functional/AutomaticUpdatesFunctionalTestBase.php +++ b/tests/src/Functional/AutomaticUpdatesFunctionalTestBase.php @@ -50,6 +50,19 @@ abstract class AutomaticUpdatesFunctionalTestBase extends BrowserTestBase { $this->disableValidators($this->disableValidators); } + /** + * {@inheritdoc} + */ + protected function tearDown(): void { + // If automatic_updates is installed, ensure any staging area created during + // the test is cleaned up. + $service_id = 'automatic_updates.updater'; + if ($this->container->has($service_id)) { + $this->container->get($service_id)->destroy(TRUE); + } + parent::tearDown(); + } + /** * Disables validators in the test site's settings. * @@ -124,10 +137,14 @@ abstract class AutomaticUpdatesFunctionalTestBase extends BrowserTestBase { /** * Asserts that we are on the "update ready" form. + * + * @param string $update_version + * The version of Drupal core that we are updating to. */ - protected function assertUpdateReady(): void { - $this->assertSession() - ->addressMatches('/\/admin\/automatic-update-ready\/[a-zA-Z0-9_\-]+$/'); + protected function assertUpdateReady(string $update_version): void { + $assert_session = $this->assertSession(); + $assert_session->addressMatches('/\/admin\/automatic-update-ready\/[a-zA-Z0-9_\-]+$/'); + $assert_session->pageTextContainsOnce('Drupal core will be updated to ' . $update_version); } } diff --git a/tests/src/Functional/ReadinessValidationTest.php b/tests/src/Functional/ReadinessValidationTest.php index ec4253ee71..c5b4fe525e 100644 --- a/tests/src/Functional/ReadinessValidationTest.php +++ b/tests/src/Functional/ReadinessValidationTest.php @@ -8,6 +8,7 @@ use Drupal\automatic_updates_test\Datetime\TestTime; use Drupal\automatic_updates_test\EventSubscriber\TestSubscriber1; use Drupal\automatic_updates_test2\EventSubscriber\TestSubscriber2; use Drupal\Core\Url; +use Drupal\package_manager_test_fixture\EventSubscriber\FixtureStager; use Drupal\system\SystemManager; use Drupal\Tests\automatic_updates\Traits\ValidationTestTrait; use Drupal\Tests\Traits\Core\CronRunTrait; @@ -398,6 +399,7 @@ class ReadinessValidationTest extends AutomaticUpdatesFunctionalTestBase { $this->container->get('module_installer')->install([ 'automatic_updates', 'automatic_updates_test', + 'package_manager_test_fixture', ]); // Because all actual staging operations are bypassed by // package_manager_bypass (enabled by the parent class), disable this @@ -418,6 +420,7 @@ class ReadinessValidationTest extends AutomaticUpdatesFunctionalTestBase { // readiness check (without storing the results), and the checker is no // longer raising an error. $this->drupalGet('/admin/modules/automatic-update'); + FixtureStager::setFixturePath(__DIR__ . '/../../fixtures/staged/9.8.1'); $assert_session->buttonExists('Update'); // Ensure that the previous results are still displayed on another admin // page, to confirm that the updater form is not discarding the previous @@ -428,7 +431,7 @@ class ReadinessValidationTest extends AutomaticUpdatesFunctionalTestBase { $this->drupalGet('/admin/modules/automatic-update'); $page->pressButton('Update'); $this->checkForMetaRefresh(); - $this->assertUpdateReady(); + $this->assertUpdateReady('9.8.1'); $page->pressButton('Continue'); $this->checkForMetaRefresh(); $assert_session->pageTextContains('Update complete!'); diff --git a/tests/src/Functional/UpdateLockTest.php b/tests/src/Functional/UpdateLockTest.php index ea3e1cc8e4..1fbf8532a6 100644 --- a/tests/src/Functional/UpdateLockTest.php +++ b/tests/src/Functional/UpdateLockTest.php @@ -2,6 +2,8 @@ namespace Drupal\Tests\automatic_updates\Functional; +use Drupal\package_manager_test_fixture\EventSubscriber\FixtureStager; + /** * Tests that only one Automatic Update operation can be performed at a time. * @@ -20,6 +22,7 @@ class UpdateLockTest extends AutomaticUpdatesFunctionalTestBase { protected static $modules = [ 'automatic_updates', 'automatic_updates_test', + 'package_manager_test_fixture', ]; /** @@ -48,9 +51,10 @@ class UpdateLockTest extends AutomaticUpdatesFunctionalTestBase { // We should be able to get partway through an update without issue. $this->drupalLogin($user_1); $this->drupalGet('/admin/modules/automatic-update'); + FixtureStager::setFixturePath(__DIR__ . '/../../fixtures/staged/9.8.1'); $page->pressButton('Update'); $this->checkForMetaRefresh(); - $this->assertUpdateReady(); + $this->assertUpdateReady('9.8.1'); $assert_session->buttonExists('Continue'); $url = parse_url($this->getSession()->getCurrentUrl(), PHP_URL_PATH); diff --git a/tests/src/Functional/UpdaterFormTest.php b/tests/src/Functional/UpdaterFormTest.php index c94abdd82b..9098ad7b45 100644 --- a/tests/src/Functional/UpdaterFormTest.php +++ b/tests/src/Functional/UpdaterFormTest.php @@ -4,11 +4,13 @@ namespace Drupal\Tests\automatic_updates\Functional; use Drupal\automatic_updates\Event\ReadinessCheckEvent; use Drupal\automatic_updates_test\Datetime\TestTime; +use Drupal\Component\FileSystem\FileSystem; use Drupal\package_manager\Event\PostRequireEvent; use Drupal\package_manager\Event\PreApplyEvent; use Drupal\package_manager\Event\PreCreateEvent; use Drupal\package_manager\ValidationResult; use Drupal\automatic_updates_test\EventSubscriber\TestSubscriber1; +use Drupal\package_manager_test_fixture\EventSubscriber\FixtureStager; use Drupal\Tests\automatic_updates\Traits\ValidationTestTrait; use Drupal\Tests\package_manager\Traits\PackageManagerBypassTestTrait; @@ -34,6 +36,7 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase { 'block', 'automatic_updates', 'automatic_updates_test', + 'package_manager_test_fixture', ]; /** @@ -250,18 +253,19 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase { $this->checkForUpdates(); $this->drupalGet('/admin/modules/automatic-update'); + FixtureStager::setFixturePath(__DIR__ . '/../../fixtures/staged/9.8.1'); $page->pressButton('Update'); $this->checkForMetaRefresh(); $this->assertUpdateStagedTimes(1); // Confirm we are on the confirmation page. - $this->assertUpdateReady(); + $this->assertUpdateReady('9.8.1'); $assert_session->buttonExists('Continue'); // If we try to return to the start page, we should be redirected back to // the confirmation page. $this->drupalGet('/admin/modules/automatic-update'); - $this->assertUpdateReady(); + $this->assertUpdateReady('9.8.1'); // Delete the existing update. $page->pressButton('Cancel update'); @@ -273,7 +277,7 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase { $this->checkForMetaRefresh(); // Confirm we are on the confirmation page. - $this->assertUpdateReady(); + $this->assertUpdateReady('9.8.1'); $this->assertUpdateStagedTimes(2); $assert_session->buttonExists('Continue'); @@ -290,7 +294,7 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase { $assert_session->pageTextNotContains($conflict_message); $page->pressButton('Update'); $this->checkForMetaRefresh(); - $this->assertUpdateReady(); + $this->assertUpdateReady('9.8.1'); // Stop execution during pre-apply. This should make Package Manager think // the staged changes are being applied and raise an error if we try to @@ -332,7 +336,7 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase { TestSubscriber1::setTestResult($results, PreApplyEvent::class); $page->pressButton('Update'); $this->checkForMetaRefresh(); - $this->assertUpdateReady(); + $this->assertUpdateReady('9.8.1'); $page->pressButton('Continue'); $this->checkForMetaRefresh(); $page->clickLink('the error page'); @@ -356,13 +360,14 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase { $page = $this->getSession()->getPage(); $this->drupalGet('/admin/modules/automatic-update'); + FixtureStager::setFixturePath(__DIR__ . '/../../fixtures/staged/9.8.1'); // The warning should be visible. $assert_session = $this->assertSession(); $assert_session->pageTextContains(reset($messages)); $page->pressButton('Update'); $this->checkForMetaRefresh(); $this->assertUpdateStagedTimes(1); - $this->assertUpdateReady(); + $this->assertUpdateReady('9.8.1'); // Simulate a staged database update in the automatic_updates_test module. // We must do this after the update has started, because the pending updates // validator will prevent an update from starting. @@ -397,10 +402,11 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase { $page = $this->getSession()->getPage(); $this->drupalGet($update_form_url); + FixtureStager::setFixturePath(__DIR__ . '/../../fixtures/staged/9.8.1'); $page->pressButton('Update'); $this->checkForMetaRefresh(); $this->assertUpdateStagedTimes(1); - $this->assertUpdateReady(); + $this->assertUpdateReady('9.8.1'); $this->assertNotTrue($this->container->get('state')->get('system.maintenance_mode')); $page->pressButton('Continue'); $this->checkForMetaRefresh(); @@ -414,6 +420,39 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase { $assert_session->pageTextContainsOnce('Update complete!'); } + /** + * Tests what happens when a staged update is deleted without being destroyed. + */ + public function testStagedUpdateDeletedImproperly(): void { + $this->setCoreVersion('9.8.0'); + $this->checkForUpdates(); + + $page = $this->getSession()->getPage(); + $this->drupalGet('/admin/modules/automatic-update'); + FixtureStager::setFixturePath(__DIR__ . '/../../fixtures/staged/9.8.1'); + $page->pressButton('Update'); + $this->checkForMetaRefresh(); + $this->assertUpdateStagedTimes(1); + $this->assertUpdateReady('9.8.1'); + // Confirm if the staged directory is deleted without using destroy(), then + // an error message will be displayed on the page. + // @see \Drupal\package_manager\Stage::getStagingRoot() + $dir = FileSystem::getOsTemporaryDirectory() . '/.package_manager' . $this->config('system.site')->get('uuid'); + $this->assertDirectoryExists($dir); + $this->container->get('file_system')->deleteRecursive($dir); + $this->getSession()->reload(); + $assert_session = $this->assertSession(); + $error_message = 'There was an error loading the pending update. Press the Cancel update button to start over.'; + $assert_session->pageTextContainsOnce($error_message); + // We should be able to start over without any problems, and the error + // message should not be seen on the updater form. + $page->pressButton('Cancel update'); + $assert_session->addressEquals('/admin/reports/updates/automatic-update'); + $assert_session->pageTextNotContains($error_message); + $assert_session->pageTextContains('The update was successfully cancelled.'); + $assert_session->buttonExists('Update'); + } + /** * Tests that the update stage is destroyed if an error occurs during require. */ -- GitLab