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