diff --git a/automatic_updates_extensions/src/Form/UpdateReady.php b/automatic_updates_extensions/src/Form/UpdateReady.php index bc0381ac4dd0d581ad369e1f06d27ded2d0434e4..270f8c2a8b2146629d98cc2777a20d9e8d1e0aa4 100644 --- a/automatic_updates_extensions/src/Form/UpdateReady.php +++ b/automatic_updates_extensions/src/Form/UpdateReady.php @@ -2,6 +2,7 @@ namespace Drupal\automatic_updates_extensions\Form; +use Drupal\automatic_updates\ProjectInfo; use Drupal\automatic_updates\Validator\StagedDatabaseUpdateValidator; use Drupal\automatic_updates_extensions\BatchProcessor; use Drupal\automatic_updates\BatchProcessor as AutoUpdatesBatchProcessor; @@ -13,6 +14,7 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\State\StateInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\package_manager\Exception\StageException; use Drupal\package_manager\Exception\StageOwnershipException; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -159,9 +161,7 @@ final class UpdateReady extends FormBase { '#type' => 'value', '#value' => $stage_id, ]; - - // @todo Display the project versions that will be update including any - // dependencies that are Drupal projects. + $form['package_updates'] = $this->showUpdates(); $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']), @@ -219,4 +219,112 @@ final class UpdateReady extends FormBase { } } + /** + * Displays all projects that will be updated. + * + * @return mixed[][] + * A render array displaying packages that will be updated. + */ + private function showUpdates(): array { + // Get packages that were updated in the staging area. + $active = $this->updater->getActiveComposer(); + $staged = $this->updater->getStageComposer(); + $updated_packages = $staged->getPackagesWithDifferentVersionsIn($active); + + // Build a list of package names that were updated by user request. + $updated_by_request = []; + foreach ($this->updater->getPackageVersions() as $group) { + $updated_by_request = array_merge($updated_by_request, array_keys($group)); + } + + $installed_packages = $active->getInstalledPackages(); + $updated_by_request_info = []; + $updated_project_info = []; + $supported_package_types = ['drupal-module', 'drupal-theme']; + + // Compile an array of relevant information about the packages that will be + // updated. + foreach ($updated_packages as $name => $updated_package) { + // Ignore anything that isn't a module or a theme. + if (!in_array($updated_package->getType(), $supported_package_types, TRUE)) { + continue; + } + $updated_project_info[$name] = [ + 'title' => $this->getProjectTitle($updated_package->getName()), + 'installed_version' => $installed_packages[$name]->getPrettyVersion(), + 'updated_version' => $updated_package->getPrettyVersion(), + ]; + } + + foreach (array_keys($updated_packages) as $name) { + // Sort the updated packages into two groups: the ones that were updated + // at the request of the user, and the ones that got updated anyway + // (probably due to Composer's dependency resolution). + if (in_array($name, $updated_by_request, TRUE)) { + $updated_by_request_info[$name] = $updated_project_info[$name]; + unset($updated_project_info[$name]); + } + } + $output = []; + if ($updated_by_request_info) { + // Create the list of messages for the packages updated by request. + $output['requested'] = $this->getUpdatedPackagesItemList($updated_by_request_info, $this->t('The following projects will be updated:')); + } + + if ($updated_project_info) { + // Create the list of messages for packages that were updated + // incidentally. + $output['dependencies'] = $this->getUpdatedPackagesItemList($updated_project_info, $this->t('The following dependencies will also be updated:')); + } + return $output; + } + + /** + * Gets the human-readable project title for a Composer package. + * + * @param string $package_name + * Package name. + * + * @return string + * The human-readable title of the project. + */ + private function getProjectTitle(string $package_name): string { + $project_name = str_replace('drupal/', '', $package_name); + $project_info = new ProjectInfo($project_name); + $project_data = $project_info->getProjectInfo(); + if ($project_data) { + return $project_data['title']; + } + else { + return $project_name; + } + } + + /** + * Generates an item list of packages that will be updated. + * + * @param array[] $updated_packages + * An array of packages that will be updated, each sub-array containing the + * project title, installed version, and target version. + * @param \Drupal\Core\StringTranslation\TranslatableMarkup $item_list_title + * The title of the generated item list. + * + * @return array + * A render array for the generated item list. + */ + private function getUpdatedPackagesItemList(array $updated_packages, TranslatableMarkup $item_list_title): array { + $create_message_for_project = function (array $project): TranslatableMarkup { + return $this->t('@title from @from_version to @to_version', [ + '@title' => $project['title'], + '@from_version' => $project['installed_version'], + '@to_version' => $project['updated_version'], + ]); + }; + return [ + '#theme' => 'item_list', + '#prefix' => '<p>' . $item_list_title . '</p>', + '#items' => array_map($create_message_for_project, $updated_packages), + ]; + } + } diff --git a/automatic_updates_extensions/tests/fixtures/stage_composer/aaa_update_test/composer.json b/automatic_updates_extensions/tests/fixtures/stage_composer/aaa_update_test/composer.json new file mode 100644 index 0000000000000000000000000000000000000000..ff9467e562f51963df37bb56bee665ec9fb08055 --- /dev/null +++ b/automatic_updates_extensions/tests/fixtures/stage_composer/aaa_update_test/composer.json @@ -0,0 +1,8 @@ +{ + "extra": { + "_readme": [ + "This fixture assumes that ../../two_projects is active directory for the test.", + "Simulates a stage directory in which aaa_update_test is updated." + ] + } +} diff --git a/automatic_updates_extensions/tests/fixtures/stage_composer/aaa_update_test/composer.lock b/automatic_updates_extensions/tests/fixtures/stage_composer/aaa_update_test/composer.lock new file mode 100644 index 0000000000000000000000000000000000000000..1baca28450cf75958ef17a9fc40cd286e5c17db1 --- /dev/null +++ b/automatic_updates_extensions/tests/fixtures/stage_composer/aaa_update_test/composer.lock @@ -0,0 +1,21 @@ +{ + "packages": [ + { + "name": "drupal/core-recommended", + "version": "9.8.0", + "require": { + "drupal/core": "9.8.0" + } + }, + { + "name": "drupal/my_module", + "version": "9.8.0" + } + ], + "packages-dev": [ + { + "name": "drupal/my_dev_module", + "version": "9.8.1" + } + ] +} diff --git a/automatic_updates_extensions/tests/fixtures/stage_composer/aaa_update_test/vendor/composer/installed.json b/automatic_updates_extensions/tests/fixtures/stage_composer/aaa_update_test/vendor/composer/installed.json new file mode 100644 index 0000000000000000000000000000000000000000..45d5a87edf512054366bbd7facada544f9e497de --- /dev/null +++ b/automatic_updates_extensions/tests/fixtures/stage_composer/aaa_update_test/vendor/composer/installed.json @@ -0,0 +1,28 @@ +{ + "packages": [ + { + "name": "drupal/core", + "version": "9.8.1", + "type": "drupal-core", + "extra": { + "drupal-scaffold": { + "file-mapping": {} + } + } + }, + { + "name": "drupal/semver_test", + "version": "8.1.0", + "type": "drupal-module" + }, + { + "name": "drupal/aaa_update_test", + "version": "2.1.0", + "type": "drupal-module" + },{ + "name": "drupal/test_theme", + "version": "2.0.0", + "type": "drupal-theme" + } + ] +} diff --git a/automatic_updates_extensions/tests/fixtures/stage_composer/semver_test/composer.json b/automatic_updates_extensions/tests/fixtures/stage_composer/semver_test/composer.json new file mode 100644 index 0000000000000000000000000000000000000000..c08df9a85fcf43be092462d8788d4fee24469c74 --- /dev/null +++ b/automatic_updates_extensions/tests/fixtures/stage_composer/semver_test/composer.json @@ -0,0 +1,8 @@ +{ + "extra": { + "_readme": [ + "This fixture assumes that ../../two_projects is active directory for the test.", + "Simulates a stage directory in which semver_test is updated." + ] + } +} diff --git a/automatic_updates_extensions/tests/fixtures/stage_composer/semver_test/composer.lock b/automatic_updates_extensions/tests/fixtures/stage_composer/semver_test/composer.lock new file mode 100644 index 0000000000000000000000000000000000000000..1baca28450cf75958ef17a9fc40cd286e5c17db1 --- /dev/null +++ b/automatic_updates_extensions/tests/fixtures/stage_composer/semver_test/composer.lock @@ -0,0 +1,21 @@ +{ + "packages": [ + { + "name": "drupal/core-recommended", + "version": "9.8.0", + "require": { + "drupal/core": "9.8.0" + } + }, + { + "name": "drupal/my_module", + "version": "9.8.0" + } + ], + "packages-dev": [ + { + "name": "drupal/my_dev_module", + "version": "9.8.1" + } + ] +} diff --git a/automatic_updates_extensions/tests/fixtures/stage_composer/semver_test/vendor/composer/installed.json b/automatic_updates_extensions/tests/fixtures/stage_composer/semver_test/vendor/composer/installed.json new file mode 100644 index 0000000000000000000000000000000000000000..9de2f48ee704f6327cfcc08ac32fd72ff6ec52ba --- /dev/null +++ b/automatic_updates_extensions/tests/fixtures/stage_composer/semver_test/vendor/composer/installed.json @@ -0,0 +1,29 @@ +{ + "packages": [ + { + "name": "drupal/core", + "version": "9.8.1", + "type": "drupal-core", + "extra": { + "drupal-scaffold": { + "file-mapping": {} + } + } + }, + { + "name": "drupal/semver_test", + "version": "8.1.1", + "type": "drupal-module" + }, + { + "name": "drupal/aaa_update_test", + "version": "2.0.0", + "type": "drupal-module" + }, + { + "name": "drupal/test_theme", + "version": "2.0.0", + "type": "drupal-theme" + } + ] +} diff --git a/automatic_updates_extensions/tests/fixtures/stage_composer/test_theme/composer.json b/automatic_updates_extensions/tests/fixtures/stage_composer/test_theme/composer.json new file mode 100644 index 0000000000000000000000000000000000000000..4ae6caae311678ec612884468ed7f52ad17fb23f --- /dev/null +++ b/automatic_updates_extensions/tests/fixtures/stage_composer/test_theme/composer.json @@ -0,0 +1,8 @@ +{ + "extra": { + "_readme": [ + "This fixture assumes that ../../two_projects is active directory for the test.", + "Simulates a stage directory in which test_theme is updated." + ] + } +} diff --git a/automatic_updates_extensions/tests/fixtures/stage_composer/test_theme/composer.lock b/automatic_updates_extensions/tests/fixtures/stage_composer/test_theme/composer.lock new file mode 100644 index 0000000000000000000000000000000000000000..1baca28450cf75958ef17a9fc40cd286e5c17db1 --- /dev/null +++ b/automatic_updates_extensions/tests/fixtures/stage_composer/test_theme/composer.lock @@ -0,0 +1,21 @@ +{ + "packages": [ + { + "name": "drupal/core-recommended", + "version": "9.8.0", + "require": { + "drupal/core": "9.8.0" + } + }, + { + "name": "drupal/my_module", + "version": "9.8.0" + } + ], + "packages-dev": [ + { + "name": "drupal/my_dev_module", + "version": "9.8.1" + } + ] +} diff --git a/automatic_updates_extensions/tests/fixtures/stage_composer/test_theme/vendor/composer/installed.json b/automatic_updates_extensions/tests/fixtures/stage_composer/test_theme/vendor/composer/installed.json new file mode 100644 index 0000000000000000000000000000000000000000..d350c2293dc557ad8beacd73edd99eb240ef3a86 --- /dev/null +++ b/automatic_updates_extensions/tests/fixtures/stage_composer/test_theme/vendor/composer/installed.json @@ -0,0 +1,29 @@ +{ + "packages": [ + { + "name": "drupal/core", + "version": "9.8.1", + "type": "drupal-core", + "extra": { + "drupal-scaffold": { + "file-mapping": {} + } + } + }, + { + "name": "drupal/semver_test", + "version": "8.1.0", + "type": "drupal-module" + }, + { + "name": "drupal/aaa_update_test", + "version": "2.0.0", + "type": "drupal-module" + }, + { + "name": "drupal/test_theme", + "version": "2.1.0", + "type": "drupal-theme" + } + ] +} diff --git a/automatic_updates_extensions/tests/fixtures/stage_composer/two_projects/composer.json b/automatic_updates_extensions/tests/fixtures/stage_composer/two_projects/composer.json new file mode 100644 index 0000000000000000000000000000000000000000..bbbc59bab3bcbeebcf81b9daaef392a27e9745c4 --- /dev/null +++ b/automatic_updates_extensions/tests/fixtures/stage_composer/two_projects/composer.json @@ -0,0 +1,8 @@ +{ + "extra": { + "_readme": [ + "This fixture assumes that ../../two_projects is active directory for the test.", + "Simulates a stage directory in which semver_test and aaa_update_test have been updated." + ] + } +} diff --git a/automatic_updates_extensions/tests/fixtures/stage_composer/two_projects/composer.lock b/automatic_updates_extensions/tests/fixtures/stage_composer/two_projects/composer.lock new file mode 100644 index 0000000000000000000000000000000000000000..1baca28450cf75958ef17a9fc40cd286e5c17db1 --- /dev/null +++ b/automatic_updates_extensions/tests/fixtures/stage_composer/two_projects/composer.lock @@ -0,0 +1,21 @@ +{ + "packages": [ + { + "name": "drupal/core-recommended", + "version": "9.8.0", + "require": { + "drupal/core": "9.8.0" + } + }, + { + "name": "drupal/my_module", + "version": "9.8.0" + } + ], + "packages-dev": [ + { + "name": "drupal/my_dev_module", + "version": "9.8.1" + } + ] +} diff --git a/automatic_updates_extensions/tests/fixtures/stage_composer/two_projects/vendor/composer/installed.json b/automatic_updates_extensions/tests/fixtures/stage_composer/two_projects/vendor/composer/installed.json new file mode 100644 index 0000000000000000000000000000000000000000..ce3af23e5c87c6ed0948c1f602ee93131d1a5d06 --- /dev/null +++ b/automatic_updates_extensions/tests/fixtures/stage_composer/two_projects/vendor/composer/installed.json @@ -0,0 +1,29 @@ +{ + "packages": [ + { + "name": "drupal/core", + "version": "9.8.1", + "type": "drupal-core", + "extra": { + "drupal-scaffold": { + "file-mapping": {} + } + } + }, + { + "name": "drupal/semver_test", + "version": "8.1.1", + "type": "drupal-module" + }, + { + "name": "drupal/aaa_update_test", + "version": "2.1.0", + "type": "drupal-module" + }, + { + "name": "drupal/test_theme", + "version": "2.0.0", + "type": "drupal-theme" + } + ] +} diff --git a/automatic_updates_extensions/tests/fixtures/two_projects/composer.lock b/automatic_updates_extensions/tests/fixtures/two_projects/composer.lock index 51df43c0cc65a352cb90e664333227a0065acac0..023d4040da6cddcdc4bde5f09633454163febcd7 100644 --- a/automatic_updates_extensions/tests/fixtures/two_projects/composer.lock +++ b/automatic_updates_extensions/tests/fixtures/two_projects/composer.lock @@ -14,6 +14,10 @@ { "name": "drupal/aaa_update_test", "version": "9.8.1" + }, + { + "name": "drupal/test_theme", + "version": "9.8.1" } ] } diff --git a/automatic_updates_extensions/tests/fixtures/two_projects/vendor/composer/installed.json b/automatic_updates_extensions/tests/fixtures/two_projects/vendor/composer/installed.json index 81dc1c71b04f960bce36c88bdc2b5f791156491e..3416572c34277d86a63cd53b1a0449c186ba12ad 100644 --- a/automatic_updates_extensions/tests/fixtures/two_projects/vendor/composer/installed.json +++ b/automatic_updates_extensions/tests/fixtures/two_projects/vendor/composer/installed.json @@ -12,17 +12,17 @@ }, { "name": "drupal/semver_test", - "version": "9.8.1", + "version": "8.1.0", "type": "drupal-module" }, { "name": "drupal/aaa_update_test", - "version": "9.8.1", + "version": "2.0.0", "type": "drupal-module" }, { "name": "drupal/test_theme", - "version": "9.8.1", + "version": "2.0.0", "type": "drupal-theme" } ] diff --git a/automatic_updates_extensions/tests/src/Functional/UpdaterFormTest.php b/automatic_updates_extensions/tests/src/Functional/UpdaterFormTest.php index b318ede904ddd8101539060aac825a730e930e37..f2ded91ed7b32683e103ef46f8044de91261086d 100644 --- a/automatic_updates_extensions/tests/src/Functional/UpdaterFormTest.php +++ b/automatic_updates_extensions/tests/src/Functional/UpdaterFormTest.php @@ -8,6 +8,7 @@ use Drupal\automatic_updates_test\StagedDatabaseUpdateValidator; use Drupal\package_manager\Event\PreApplyEvent; use Drupal\package_manager\ValidationResult; use Drupal\package_manager_bypass\Beginner; +use Drupal\package_manager_bypass\Stager; use Drupal\Tests\automatic_updates\Functional\AutomaticUpdatesFunctionalTestBase; use Drupal\Tests\automatic_updates\Traits\ValidationTestTrait; use Drupal\Tests\automatic_updates_extensions\Traits\FormTestTrait; @@ -122,9 +123,11 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase { * The expected installed version. * @param string $expected_target_version * The expected target version. + * @param int $row + * The row number. */ - private function assertTableShowsUpdates(string $expected_project_title, string $expected_installed_version, string $expected_target_version): void { - $this->assertUpdateTableRow($this->assertSession(), $expected_project_title, $expected_installed_version, $expected_target_version); + private function assertTableShowsUpdates(string $expected_project_title, string $expected_installed_version, string $expected_target_version, int $row = 1): void { + $this->assertUpdateTableRow($this->assertSession(), $expected_project_title, $expected_installed_version, $expected_target_version, $row); } /** @@ -154,13 +157,13 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase { */ public function testSuccessfulUpdate(bool $maintenance_mode_on, string $project_name, string $project_title, string $installed_version, string $target_version): void { $this->container->get('theme_installer')->install(['automatic_updates_theme_with_updates']); - $this->updateProject = $project_name; // By default, the Update module only checks for updates of installed modules // and themes. The two modules we're testing here (semver_test and aaa_update_test) // are already installed by static::$modules. $this->container->get('theme_installer')->install(['test_theme']); + Stager::setFixturePath(__DIR__ . '/../../fixtures/stage_composer/' . $project_name); $this->setReleaseMetadata(__DIR__ . '/../../../../tests/fixtures/release-history/drupal.9.8.2.xml'); - $this->setReleaseMetadata(__DIR__ . "/../../fixtures/release-history/$project_name.1.1.xml"); + $this->setReleaseMetadata(__DIR__ . '/../../fixtures/release-history/' . $project_name . '.1.1.xml'); $this->setProjectInstalledVersion([$project_name => $installed_version]); $this->checkForUpdates(); $state = $this->container->get('state'); @@ -184,13 +187,14 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase { $assert_session->pageTextContains('Please select one or more projects.'); // Submit with a project selected. - $page->checkField('projects[' . $this->updateProject . ']'); + $page->checkField('projects[' . $project_name . ']'); $page->pressButton('Update'); $this->checkForMetaRefresh(); $this->assertUpdateStagedTimes(1); // Confirm that the site was put into maintenance mode if needed. $this->assertSame($state->get('system.maintenance_mode'), $maintenance_mode_on); + $assert_session->pageTextNotContains('The following dependencies will also be updated:'); // Ensure that a list of pending database updates is visible, along with a // short explanation, in the warning messages. $warning_messages = $assert_session->elementExists('xpath', '//div[@data-drupal-messages]//div[@aria-label="Warning message"]'); @@ -212,6 +216,79 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase { $this->assertSame($state->get('system.maintenance_mode'), $maintenance_mode_on); } + /** + * Data provider for testDisplayUpdates(). + * + * @return array[] + * The test cases. + */ + public function providerDisplayUpdates(): array { + return [ + 'with unrequested updates' => [TRUE], + 'without unrequested updates' => [FALSE], + ]; + } + + /** + * Tests the form displays the correct projects which will be updated. + * + * @param bool $unrequested_updates + * Whether unrequested updates are present during update. + * + * @dataProvider providerDisplayUpdates + */ + public function testDisplayUpdates(bool $unrequested_updates): void { + $this->container->get('theme_installer')->install(['automatic_updates_theme_with_updates']); + $this->setReleaseMetadata(__DIR__ . '/../../../../tests/fixtures/release-history/drupal.9.8.2.xml'); + $this->setReleaseMetadata(__DIR__ . "/../../fixtures/release-history/semver_test.1.1.xml"); + $this->setReleaseMetadata(__DIR__ . "/../../fixtures/release-history/aaa_update_test.1.1.xml"); + Stager::setFixturePath(__DIR__ . '/../../fixtures/stage_composer/two_projects'); + $this->setProjectInstalledVersion([ + 'semver_test' => '8.1.0', + 'aaa_update_test' => '8.x-2.0', + ]); + $this->checkForUpdates(); + $state = $this->container->get('state'); + $page = $this->getSession()->getPage(); + + // Navigate to the automatic updates form. + $this->drupalGet('/admin/reports/updates'); + $this->clickLink('Update Extensions'); + $this->assertTableShowsUpdates( + 'AAA Update test', + '8.x-2.0', + '8.x-2.1', + ); + $this->assertTableShowsUpdates( + 'Semver Test', + '8.1.0', + '8.1.1', + 2 + ); + // User will choose both the projects to update and there will be no + // unrequested updates. + if ($unrequested_updates === FALSE) { + $page->checkField('projects[aaa_update_test]'); + } + $page->checkField('projects[semver_test]'); + $page->pressButton('Update'); + $this->checkForMetaRefresh(); + $this->assertUpdateStagedTimes(1); + $assert_session = $this->assertSession(); + // Both projects will be shown as requested updates if there are no + // unrequested updates, otherwise one project which user chose will be shown + // as requested update and other one will be shown as unrequested update. + if ($unrequested_updates === FALSE) { + $assert_session->pageTextNotContains('The following dependencies will also be updated:'); + } + else { + $assert_session->pageTextContains('The following dependencies will also be updated:'); + } + $assert_session->pageTextContains('The following projects will be updated:'); + $assert_session->pageTextContains('Semver Test from 8.1.0 to 8.1.1'); + $assert_session->pageTextContains('AAA Update test from 2.0.0 to 2.1.0'); + } + /** * Tests the form when modules requiring an update not installed via composer. */