From eb064d8c6c840da3f79bb506772c3b5db4957fba Mon Sep 17 00:00:00 2001 From: "kunal.sachdev" <kunal.sachdev@3685163.no-reply.drupal.org> Date: Thu, 9 Jun 2022 13:22:16 +0000 Subject: [PATCH] Issue #3273017 by kunal.sachdev, tedbow: Create a validator to confirm that extensions being updated were installed via Composer --- .../automatic_updates_extensions.services.yml | 6 + ...PackagesInstalledWithComposerValidator.php | 97 ++++++++++ .../fixtures/new_module/1.0.0/composer.json | 5 + .../new_module/1.0.0/new_module.info.yml | 3 + .../fixtures/new_module/1.1.0/composer.json | 5 + .../new_module/1.1.0/new_module.info.yml | 3 + .../active.installed.json | 44 +++++ ...module_not_installed.staged.installed.json | 34 ++++ ...ndency_not_installed.staged.installed.json | 54 ++++++ ...rofile_not_installed.staged.installed.json | 34 ++++ .../theme_not_installed.staged.installed.json | 34 ++++ .../release-history/new_module.1.1.0.xml | 40 ++++ .../tests/src/Build/ModuleUpdateTest.php | 49 ++++- .../tests/src/Functional/UpdaterFormTest.php | 5 + ...tomaticUpdatesExtensionsKernelTestBase.php | 43 ++++- ...agesInstalledWithComposerValidatorTest.php | 180 ++++++++++++++++++ .../Validator/UpdateReleaseValidatorTest.php | 8 + .../tests/src/Traits/FormTestTrait.php | 12 +- 18 files changed, 642 insertions(+), 14 deletions(-) create mode 100644 automatic_updates_extensions/src/Validator/PackagesInstalledWithComposerValidator.php create mode 100644 automatic_updates_extensions/tests/fixtures/new_module/1.0.0/composer.json create mode 100644 automatic_updates_extensions/tests/fixtures/new_module/1.0.0/new_module.info.yml create mode 100644 automatic_updates_extensions/tests/fixtures/new_module/1.1.0/composer.json create mode 100644 automatic_updates_extensions/tests/fixtures/new_module/1.1.0/new_module.info.yml create mode 100644 automatic_updates_extensions/tests/fixtures/packages_installed_with_composer_validator/active.installed.json create mode 100644 automatic_updates_extensions/tests/fixtures/packages_installed_with_composer_validator/module_not_installed.staged.installed.json create mode 100644 automatic_updates_extensions/tests/fixtures/packages_installed_with_composer_validator/module_theme_profile_dependency_not_installed.staged.installed.json create mode 100644 automatic_updates_extensions/tests/fixtures/packages_installed_with_composer_validator/profile_not_installed.staged.installed.json create mode 100644 automatic_updates_extensions/tests/fixtures/packages_installed_with_composer_validator/theme_not_installed.staged.installed.json create mode 100644 automatic_updates_extensions/tests/fixtures/release-history/new_module.1.1.0.xml create mode 100644 automatic_updates_extensions/tests/src/Kernel/Validator/PackagesInstalledWithComposerValidatorTest.php diff --git a/automatic_updates_extensions/automatic_updates_extensions.services.yml b/automatic_updates_extensions/automatic_updates_extensions.services.yml index dea4d1ff45..a8cadd7798 100644 --- a/automatic_updates_extensions/automatic_updates_extensions.services.yml +++ b/automatic_updates_extensions/automatic_updates_extensions.services.yml @@ -11,6 +11,12 @@ services: - '@event_dispatcher' - '@tempstore.shared' - '@datetime.time' + automatic_updates_extensions.validator.packages_installed_with_composer: + class: Drupal\automatic_updates_extensions\Validator\PackagesInstalledWithComposerValidator + arguments: + - '@string_translation' + tags: + - { name: event_subscriber } automatic_updates_extensions.validator.target_release: class: Drupal\automatic_updates_extensions\Validator\UpdateReleaseValidator tags: diff --git a/automatic_updates_extensions/src/Validator/PackagesInstalledWithComposerValidator.php b/automatic_updates_extensions/src/Validator/PackagesInstalledWithComposerValidator.php new file mode 100644 index 0000000000..82ac5d8897 --- /dev/null +++ b/automatic_updates_extensions/src/Validator/PackagesInstalledWithComposerValidator.php @@ -0,0 +1,97 @@ +<?php + +namespace Drupal\automatic_updates_extensions\Validator; + +use Composer\Package\PackageInterface; +use Drupal\automatic_updates_extensions\ExtensionUpdater; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\StringTranslation\TranslationInterface; +use Drupal\package_manager\Event\PreApplyEvent; +use Drupal\package_manager\Event\PreCreateEvent; +use Drupal\package_manager\Event\PreOperationStageEvent; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * Validates packages are installed via Composer. + */ +class PackagesInstalledWithComposerValidator implements EventSubscriberInterface { + + use StringTranslationTrait; + + /** + * Constructs a InstalledPackagesValidator object. + * + * @param \Drupal\Core\StringTranslation\TranslationInterface $translation + * The translation service. + */ + public function __construct(TranslationInterface $translation) { + $this->setStringTranslation($translation); + } + + /** + * Validates that packages are installed with composer or not. + * + * @param \Drupal\package_manager\Event\PreOperationStageEvent $event + * The event object. + */ + public function checkPackagesInstalledWithComposer(PreOperationStageEvent $event): void { + $stage = $event->getStage(); + if ($stage instanceof ExtensionUpdater) { + $active_composer = $stage->getActiveComposer(); + $installed_packages = $active_composer->getInstalledPackages(); + $missing_packages = []; + if ($event instanceof PreCreateEvent) { + $package_versions = $stage->getPackageVersions(); + foreach (['production', 'dev'] as $package_type) { + $missing_packages = array_merge($missing_packages, array_diff_key($package_versions[$package_type], $installed_packages)); + } + } + else { + $missing_packages = $stage->getStageComposer() + ->getPackagesNotIn($active_composer); + // For new dependency added in the stage will are only concerned with + // ones that are Drupal projects that have Update XML from Drupal.org + // Since the Update module does allow use to check any of these projects + // if they don't exist in the active code base. Other types of projects + // even if they are in the 'drupal/' namespace they would not have + // Update XML on Drupal.org so it doesn't matter if they are in the + // active codebase or not. + $types = [ + 'drupal-module', + 'drupal-theme', + 'drupal-profile', + ]; + $filter = function (PackageInterface $package) use ($types): bool { + return in_array($package->getType(), $types); + }; + $missing_packages = array_filter($missing_packages, $filter); + // Saving only the packages whose name starts with drupal/. + $missing_packages = array_filter($missing_packages, function (string $key) { + return strpos($key, 'drupal/') === 0; + }, ARRAY_FILTER_USE_KEY); + } + if ($missing_packages) { + $missing_projects = []; + foreach ($missing_packages as $package => $version) { + // Removing drupal/ from package name for better user presentation. + $project = str_replace('drupal/', '', $package); + $missing_projects[] = $project; + } + if ($missing_projects) { + $event->addError($missing_projects, $this->t('Automatic Updates can only update projects that were installed via Composer. The following packages are not installed through composer:')); + } + } + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + return [ + PreCreateEvent::class => 'checkPackagesInstalledWithComposer', + PreApplyEvent::class => 'checkPackagesInstalledWithComposer', + ]; + } + +} diff --git a/automatic_updates_extensions/tests/fixtures/new_module/1.0.0/composer.json b/automatic_updates_extensions/tests/fixtures/new_module/1.0.0/composer.json new file mode 100644 index 0000000000..824b16f969 --- /dev/null +++ b/automatic_updates_extensions/tests/fixtures/new_module/1.0.0/composer.json @@ -0,0 +1,5 @@ +{ + "name": "drupal/new_module", + "type": "drupal-module", + "version": "1.0.0" +} diff --git a/automatic_updates_extensions/tests/fixtures/new_module/1.0.0/new_module.info.yml b/automatic_updates_extensions/tests/fixtures/new_module/1.0.0/new_module.info.yml new file mode 100644 index 0000000000..8ffd0cce5c --- /dev/null +++ b/automatic_updates_extensions/tests/fixtures/new_module/1.0.0/new_module.info.yml @@ -0,0 +1,3 @@ +name: 'New module' +type: module +core_version_requirement: ^9 diff --git a/automatic_updates_extensions/tests/fixtures/new_module/1.1.0/composer.json b/automatic_updates_extensions/tests/fixtures/new_module/1.1.0/composer.json new file mode 100644 index 0000000000..9d2d4e95a6 --- /dev/null +++ b/automatic_updates_extensions/tests/fixtures/new_module/1.1.0/composer.json @@ -0,0 +1,5 @@ +{ + "name": "drupal/new_module", + "type": "drupal-module", + "version": "1.1.0" +} diff --git a/automatic_updates_extensions/tests/fixtures/new_module/1.1.0/new_module.info.yml b/automatic_updates_extensions/tests/fixtures/new_module/1.1.0/new_module.info.yml new file mode 100644 index 0000000000..8ffd0cce5c --- /dev/null +++ b/automatic_updates_extensions/tests/fixtures/new_module/1.1.0/new_module.info.yml @@ -0,0 +1,3 @@ +name: 'New module' +type: module +core_version_requirement: ^9 diff --git a/automatic_updates_extensions/tests/fixtures/packages_installed_with_composer_validator/active.installed.json b/automatic_updates_extensions/tests/fixtures/packages_installed_with_composer_validator/active.installed.json new file mode 100644 index 0000000000..a220e444fa --- /dev/null +++ b/automatic_updates_extensions/tests/fixtures/packages_installed_with_composer_validator/active.installed.json @@ -0,0 +1,44 @@ +{ + "packages": [ + { + "name": "drupal/core-recommended", + "version": "9.8.0", + "require": { + "drupal/core": "9.8.0" + } + }, + { + "name": "drupal/core", + "version": "9.8.0" + }, + { + "name": "drupal/my_module", + "version": "9.8.0", + "type": "drupal-module" + }, + { + "name": "drupal/my_dev_module", + "version": "9.8.1", + "type": "drupal-module" + }, + { + "name": "drupal/existing_module", + "version": "9.8.0", + "type": "drupal-module" + }, + { + "name": "drupal/existing_theme", + "version": "9.8.0", + "type": "drupal-theme" + }, + { + "name": "drupal/existing_profile", + "version": "9.8.0", + "type": "drupal-profile" + } + ], + "dev": true, + "dev-package-names": [ + "drupal/my_dev_module" + ] +} diff --git a/automatic_updates_extensions/tests/fixtures/packages_installed_with_composer_validator/module_not_installed.staged.installed.json b/automatic_updates_extensions/tests/fixtures/packages_installed_with_composer_validator/module_not_installed.staged.installed.json new file mode 100644 index 0000000000..3ad2ba801c --- /dev/null +++ b/automatic_updates_extensions/tests/fixtures/packages_installed_with_composer_validator/module_not_installed.staged.installed.json @@ -0,0 +1,34 @@ +{ + "packages": [ + { + "name": "drupal/core-recommended", + "version": "9.8.0", + "require": { + "drupal/core": "9.8.0" + } + }, + { + "name": "drupal/core", + "version": "9.8.0" + }, + { + "name": "drupal/my_module", + "version": "9.8.0", + "type": "drupal-module" + }, + { + "name": "drupal/my_dev_module", + "version": "9.8.1", + "type": "drupal-module" + }, + { + "name": "drupal/new_module", + "version": "9.8.0", + "type": "drupal-module" + } + ], + "dev": true, + "dev-package-names": [ + "drupal/my_dev_module" + ] +} diff --git a/automatic_updates_extensions/tests/fixtures/packages_installed_with_composer_validator/module_theme_profile_dependency_not_installed.staged.installed.json b/automatic_updates_extensions/tests/fixtures/packages_installed_with_composer_validator/module_theme_profile_dependency_not_installed.staged.installed.json new file mode 100644 index 0000000000..c6b553899b --- /dev/null +++ b/automatic_updates_extensions/tests/fixtures/packages_installed_with_composer_validator/module_theme_profile_dependency_not_installed.staged.installed.json @@ -0,0 +1,54 @@ +{ + "packages": [ + { + "name": "drupal/core-recommended", + "version": "9.8.0", + "require": { + "drupal/core": "9.8.0" + } + }, + { + "name": "drupal/core", + "version": "9.8.0" + }, + { + "name": "drupal/my_module", + "version": "9.8.0", + "type": "drupal-module" + }, + { + "name": "drupal/my_dev_module", + "version": "9.8.1", + "type": "drupal-module" + }, + { + "name": "drupal/new_module", + "version": "9.8.0", + "type": "drupal-module" + }, + { + "name": "not-drupal/new_module1", + "version": "9.8.0", + "type": "drupal-module" + }, + { + "name": "drupal/new_theme", + "version": "9.8.0", + "type": "drupal-theme" + }, + { + "name": "drupal/new_profile", + "version": "9.8.0", + "type": "drupal-profile" + }, + { + "name": "drupal/new_dependency", + "version": "9.8.0", + "type": "drupal-library" + } + ], + "dev": true, + "dev-package-names": [ + "drupal/my_dev_module" + ] +} diff --git a/automatic_updates_extensions/tests/fixtures/packages_installed_with_composer_validator/profile_not_installed.staged.installed.json b/automatic_updates_extensions/tests/fixtures/packages_installed_with_composer_validator/profile_not_installed.staged.installed.json new file mode 100644 index 0000000000..c4c8db4e54 --- /dev/null +++ b/automatic_updates_extensions/tests/fixtures/packages_installed_with_composer_validator/profile_not_installed.staged.installed.json @@ -0,0 +1,34 @@ +{ + "packages": [ + { + "name": "drupal/core-recommended", + "version": "9.8.0", + "require": { + "drupal/core": "9.8.0" + } + }, + { + "name": "drupal/core", + "version": "9.8.0" + }, + { + "name": "drupal/my_module", + "version": "9.8.0", + "type": "drupal-module" + }, + { + "name": "drupal/my_dev_module", + "version": "9.8.1", + "type": "drupal-module" + }, + { + "name": "drupal/new_profile", + "version": "9.8.0", + "type": "drupal-profile" + } + ], + "dev": true, + "dev-package-names": [ + "drupal/my_dev_module" + ] +} diff --git a/automatic_updates_extensions/tests/fixtures/packages_installed_with_composer_validator/theme_not_installed.staged.installed.json b/automatic_updates_extensions/tests/fixtures/packages_installed_with_composer_validator/theme_not_installed.staged.installed.json new file mode 100644 index 0000000000..e09bb8a0fd --- /dev/null +++ b/automatic_updates_extensions/tests/fixtures/packages_installed_with_composer_validator/theme_not_installed.staged.installed.json @@ -0,0 +1,34 @@ +{ + "packages": [ + { + "name": "drupal/core-recommended", + "version": "9.8.0", + "require": { + "drupal/core": "9.8.0" + } + }, + { + "name": "drupal/core", + "version": "9.8.0" + }, + { + "name": "drupal/my_module", + "version": "9.8.0", + "type": "drupal-module" + }, + { + "name": "drupal/my_dev_module", + "version": "9.8.1", + "type": "drupal-module" + }, + { + "name": "drupal/new_theme", + "version": "9.8.0", + "type": "drupal-theme" + } + ], + "dev": true, + "dev-package-names": [ + "drupal/my_dev_module" + ] +} diff --git a/automatic_updates_extensions/tests/fixtures/release-history/new_module.1.1.0.xml b/automatic_updates_extensions/tests/fixtures/release-history/new_module.1.1.0.xml new file mode 100644 index 0000000000..0557a19b2d --- /dev/null +++ b/automatic_updates_extensions/tests/fixtures/release-history/new_module.1.1.0.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<project xmlns:dc="http://purl.org/dc/elements/1.1/"> +<title>New Module</title> +<short_name>new_module</short_name> +<dc:creator>Drupal</dc:creator> +<supported_branches>1.1.,1.0.</supported_branches> +<project_status>published</project_status> +<link>http://example.com/project/new_module</link> + <terms> + <term><name>Projects</name><value>New Module project</value></term> + </terms> +<releases> + <release> + <name>New Module 1.1.0</name> + <version>1.1.0</version> + <tag>1.1.0</tag> + <status>published</status> + <release_link>http://example.com/new_module-1-1-0-release</release_link> + <download_link>http://example.com/new_module-1-1-0.tar.gz</download_link> + <date>1584195300</date> + <terms> + <term><name>Release type</name><value>New features</value></term> + <term><name>Release type</name><value>Bug fixes</value></term> + </terms> + </release> + <release> + <name>New Module 1.0.0</name> + <version>1.0.0</version> + <tag>1.0.0</tag> + <status>published</status> + <release_link>http://example.com/new_module-1-0-0-release</release_link> + <download_link>http://example.com/new_module-1-0-0.tar.gz</download_link> + <date>1581603300</date> + <terms> + <term><name>Release type</name><value>New features</value></term> + <term><name>Release type</name><value>Bug fixes</value></term> + </terms> + </release> +</releases> +</project> diff --git a/automatic_updates_extensions/tests/src/Build/ModuleUpdateTest.php b/automatic_updates_extensions/tests/src/Build/ModuleUpdateTest.php index f66f3b81f3..db4ed3b00f 100644 --- a/automatic_updates_extensions/tests/src/Build/ModuleUpdateTest.php +++ b/automatic_updates_extensions/tests/src/Build/ModuleUpdateTest.php @@ -4,6 +4,7 @@ namespace Drupal\Tests\automatic_updates_extensions\Build; use Drupal\Tests\automatic_updates\Build\UpdateTestBase; use Drupal\Tests\automatic_updates_extensions\Traits\FormTestTrait; +use Symfony\Component\Filesystem\Filesystem as SymfonyFilesystem; /** * Tests updating modules in a staging area. @@ -22,11 +23,15 @@ class ModuleUpdateTest extends UpdateTestBase { $this->setReleaseMetadata([ 'drupal' => __DIR__ . '/../../../../tests/fixtures/release-history/drupal.9.8.1-security.xml', 'alpha' => __DIR__ . '/../../fixtures/release-history/alpha.1.1.0.xml', + 'new_module' => __DIR__ . '/../../fixtures/release-history/new_module.1.1.0.xml', ]); - // Set 'version' and 'project' for the 'alpha' module to enable the Update - // to determine the update status. - $system_info = ['alpha' => ['version' => '1.0.0', 'project' => 'alpha']]; + // Set 'version' and 'project' for the 'alpha' and 'new_module' module to + // enable the Update to determine the update status. + $system_info = [ + 'alpha' => ['version' => '1.0.0', 'project' => 'alpha'], + 'new_module' => ['version' => '1.0.0', 'project' => 'new_module'], + ]; $system_info = var_export($system_info, TRUE); $code = <<<END \$config['update_test.settings']['system_info'] = $system_info; @@ -36,8 +41,13 @@ END; $this->addRepository('alpha', __DIR__ . '/../../../../package_manager/tests/fixtures/alpha/1.0.0'); $this->runComposer('COMPOSER_MIRROR_PATH_REPOS=1 composer require drupal/alpha --update-with-all-dependencies', 'project'); $this->assertModuleVersion('alpha', '1.0.0'); - - $this->installModules(['automatic_updates_extensions_test_api', 'alpha']); + $fs = new SymfonyFilesystem(); + $fs->mirror(__DIR__ . '/../../fixtures/new_module', $this->getWorkspaceDirectory() . '/project/web/modules'); + $this->installModules([ + 'automatic_updates_extensions_test_api', + 'alpha', + 'new_module', + ]); // Change both modules' upstream version. $this->addRepository('alpha', __DIR__ . '/../../../../package_manager/tests/fixtures/alpha/1.1.0'); @@ -48,7 +58,22 @@ END; */ public function testApi(): void { $this->createTestProject('RecommendedProject'); - + // Use the API endpoint to create a stage and update the 'new_module' module + // to 1.1.0. + // @see \Drupal\automatic_updates_extensions_test_api\ApiController::run() + // There will be error in updating as this module is not installed + // via composer @see \Drupal\Tests\automatic_updates_extensions\Kernel\Validator\PackagesInstalledWithComposerValidatorTest. + $query = http_build_query([ + 'projects' => [ + 'new_module' => '1.1.0', + ], + ]); + $this->visit("/automatic-updates-extensions-test-api?$query"); + $mink = $this->getMink(); + $mink->assertSession()->statusCodeEquals(500); + $page_text = $mink->getSession()->getPage()->getText(); + $this->assertStringContainsString('Automatic Updates can only update projects that were installed via Composer. The following packages are not installed through composer:', $page_text); + $this->assertStringContainsString('new_module', $page_text); // Use the API endpoint to create a stage and update the 'alpha' module to // 1.1.0. We ask the API to return the contents of the module's // composer.json file, so we can assert that they were updated to the @@ -86,7 +111,17 @@ END; $this->visit('/admin/reports/updates'); $page->clickLink('Update Extensions'); - $this->assertUpdateTableRow($assert_session, 'Alpha', '1.0.0', '1.1.0'); + $this->assertUpdateTableRow($assert_session, 'Alpha', '1.0.0', '1.1.0', 2); + $this->assertUpdateTableRow($assert_session, 'New module', '1.0.0', '1.1.0', 1); + $page->checkField('projects[new_module]'); + $page->pressButton('Update'); + $this->waitForBatchJob(); + $page_text = $page->getText(); + // There will be error in updating 'new_module' as it is not installed via + // composer @see \Drupal\Tests\automatic_updates_extensions\Kernel\Validator\PackagesInstalledWithComposerValidatorTest. + $this->assertStringContainsString('Automatic Updates can only update projects that were installed via Composer. The following packages are not installed through composer:', $page_text); + $this->assertStringContainsString('new_module', $page_text); + $page->clickLink('error page'); $page->checkField('projects[alpha]'); $page->pressButton('Update'); $this->waitForBatchJob(); diff --git a/automatic_updates_extensions/tests/src/Functional/UpdaterFormTest.php b/automatic_updates_extensions/tests/src/Functional/UpdaterFormTest.php index 6d56d9fde2..b4212ed7cc 100644 --- a/automatic_updates_extensions/tests/src/Functional/UpdaterFormTest.php +++ b/automatic_updates_extensions/tests/src/Functional/UpdaterFormTest.php @@ -62,6 +62,11 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase { * {@inheritdoc} */ protected function setUp(): void { + // In this test class, some modules are added and this validator will + // complain because these are not installed via composer. This validator + // already has test coverage. + // @see \Drupal\Tests\automatic_updates_extensions\Build\ModuleUpdateTest + $this->disableValidators[] = 'automatic_updates_extensions.validator.packages_installed_with_composer'; parent::setUp(); $this->setReleaseMetadata(__DIR__ . '/../../fixtures/release-history/semver_test.1.1.xml'); $user = $this->createUser([ diff --git a/automatic_updates_extensions/tests/src/Kernel/AutomaticUpdatesExtensionsKernelTestBase.php b/automatic_updates_extensions/tests/src/Kernel/AutomaticUpdatesExtensionsKernelTestBase.php index 64a276ec88..fcd14e0bfc 100644 --- a/automatic_updates_extensions/tests/src/Kernel/AutomaticUpdatesExtensionsKernelTestBase.php +++ b/automatic_updates_extensions/tests/src/Kernel/AutomaticUpdatesExtensionsKernelTestBase.php @@ -3,6 +3,8 @@ namespace Drupal\Tests\automatic_updates_extensions\Kernel; use Drupal\automatic_updates_extensions\ExtensionUpdater; + +use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\package_manager\Exception\StageValidationException; use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase; use Drupal\Tests\package_manager\Kernel\TestStageTrait; @@ -26,6 +28,43 @@ abstract class AutomaticUpdatesExtensionsKernelTestBase extends AutomaticUpdates 'automatic_updates_test_release_history', ]; + /** + * {@inheritdoc} + */ + public function register(ContainerBuilder $container) { + parent::register($container); + + // Use the test-only implementations of the regular and cron updaters. + $overrides = [ + 'automatic_updates_extensions.updater' => TestExtensionUpdater::class, + ]; + foreach ($overrides as $service_id => $class) { + if ($container->hasDefinition($service_id)) { + $container->getDefinition($service_id)->setClass($class); + } + } + } + + /** + * Creates a stage object for testing purposes. + * + * @return \Drupal\automatic_updates_extensions\ExtensionUpdater + * A stage object, with test-only modifications. + */ + protected function createUpdater(): ExtensionUpdater { + return new TestExtensionUpdater( + $this->container->get('config.factory'), + $this->container->get('package_manager.path_locator'), + $this->container->get('package_manager.beginner'), + $this->container->get('package_manager.stager'), + $this->container->get('package_manager.committer'), + $this->container->get('file_system'), + $this->container->get('event_dispatcher'), + $this->container->get('tempstore.shared'), + $this->container->get('datetime.time') + ); + } + /** * The client. * @@ -44,7 +83,7 @@ abstract class AutomaticUpdatesExtensionsKernelTestBase extends AutomaticUpdates * (optional) The class of the event which should return the results. Must * be passed if $expected_results is not empty. */ - protected function assertUpdaterResults(array $project_versions, array $expected_results, string $event_class = NULL): void { + protected function assertUpdateResults(array $project_versions, array $expected_results, string $event_class = NULL): void { $updater = $this->createExtensionUpdater(); try { @@ -117,7 +156,7 @@ abstract class AutomaticUpdatesExtensionsKernelTestBase extends AutomaticUpdates } /** - * Defines a updater specifically for testing purposes. + * A test-only version of the regular extension updater to override internals. */ class TestExtensionUpdater extends ExtensionUpdater { diff --git a/automatic_updates_extensions/tests/src/Kernel/Validator/PackagesInstalledWithComposerValidatorTest.php b/automatic_updates_extensions/tests/src/Kernel/Validator/PackagesInstalledWithComposerValidatorTest.php new file mode 100644 index 0000000000..6f8b88b45f --- /dev/null +++ b/automatic_updates_extensions/tests/src/Kernel/Validator/PackagesInstalledWithComposerValidatorTest.php @@ -0,0 +1,180 @@ +<?php + +namespace Drupal\Tests\automatic_updates_extensions\Kernel\Validator; + +use Drupal\package_manager\Event\PreApplyEvent; +use Drupal\package_manager\Event\PreCreateEvent; +use Drupal\package_manager\ValidationResult; +use Drupal\Tests\automatic_updates_extensions\Kernel\AutomaticUpdatesExtensionsKernelTestBase; + +/** + * Validates the installed packages via composer after an update. + * + * @coversDefaultClass \Drupal\automatic_updates_extensions\Validator\PackagesInstalledWithComposerValidator + * + * @group automatic_updates_extensions + */ +class PackagesInstalledWithComposerValidatorTest extends AutomaticUpdatesExtensionsKernelTestBase { + + /** + * The active directory in the virtual file system. + * + * @var string + */ + private $activeDir; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + // In this test, we don't focus on validating that the updated projects are + // secure and supported. Therefore, we need to disable the update release + // validator that validates updated projects are secure and supported. + $this->disableValidators[] = 'automatic_updates_extensions.validator.target_release'; + parent::setUp(); + $this->createTestProject(); + $this->activeDir = $this->container->get('package_manager.path_locator') + ->getProjectRoot(); + } + + /** + * Data provider for testPreCreateException(). + * + * @return array + * Test cases for testPreCreateException(). + */ + public function providerPreCreateException(): array { + return [ + 'module not installed via composer' => [ + [ + 'new_module' => '9.8.0', + ], + [ValidationResult::createError(['new_module'], t('Automatic Updates can only update projects that were installed via Composer. The following packages are not installed through composer:'))], + ], + 'theme not installed via composer' => [ + [ + 'new_theme' => '9.8.0', + ], + [ValidationResult::createError(['new_theme'], t('Automatic Updates can only update projects that were installed via Composer. The following packages are not installed through composer:'))], + ], + 'profile not installed via composer' => [ + [ + 'new_profile' => '9.8.0', + ], + [ValidationResult::createError(['new_profile'], t('Automatic Updates can only update projects that were installed via Composer. The following packages are not installed through composer:'))], + ], + 'module_theme_profile_dependency_not_installed_via_composer' => [ + [ + 'new_module' => '9.8.0', + 'new_theme' => '9.8.0', + 'new_profile' => '9.8.0', + 'new_dependency' => '9.8.0', + ], + [ + ValidationResult::createError( + ['new_module', 'new_theme', 'new_profile', 'new_dependency'], + t('Automatic Updates can only update projects that were installed via Composer. The following packages are not installed through composer:')), + ], + ], + 'module_theme_profile_installed_via_composer' => [ + [ + 'existing_module' => '9.8.1', + 'existing_theme' => '9.8.1', + 'existing_profile' => '9.8.1', + ], + [], + ], + 'existing module installed and new module not installed via composer' => [ + [ + 'existing_module' => '9.8.1', + 'new_module' => '9.8.0', + ], + [ValidationResult::createError(['new_module'], t('Automatic Updates can only update projects that were installed via Composer. The following packages are not installed through composer:'))], + ], + ]; + } + + /** + * Tests the packages installed with composer during pre-create. + * + * @param array $projects + * The projects to install. + * @param array $expected_results + * The expected validation results. + * + * @dataProvider providerPreCreateException + */ + public function testPreCreateException(array $projects, array $expected_results): void { + // Path of `active.installed.json` file. It will be used as the virtual + // project's active `vendor/composer/installed.json` file. + $active_installed = __DIR__ . '/../../../fixtures/packages_installed_with_composer_validator/active.installed.json'; + $this->assertFileIsReadable($active_installed); + copy($active_installed, "$this->activeDir/vendor/composer/installed.json"); + $this->assertUpdateResults($projects, $expected_results, PreCreateEvent::class); + } + + /** + * Data provider for testPreApplyException(). + * + * @return array + * Test cases for testPreApplyException(). + */ + public function providerPreApplyException(): array { + $fixtures_folder = __DIR__ . '/../../../fixtures/packages_installed_with_composer_validator'; + return [ + 'module not installed via composer' => [ + "$fixtures_folder/module_not_installed.staged.installed.json", + [ValidationResult::createError(['new_module'], t('Automatic Updates can only update projects that were installed via Composer. The following packages are not installed through composer:'))], + ], + 'theme not installed via composer' => [ + "$fixtures_folder/theme_not_installed.staged.installed.json", + [ValidationResult::createError(['new_theme'], t('Automatic Updates can only update projects that were installed via Composer. The following packages are not installed through composer:'))], + ], + 'profile not installed via composer' => [ + "$fixtures_folder/profile_not_installed.staged.installed.json", + [ValidationResult::createError(['new_profile'], t('Automatic Updates can only update projects that were installed via Composer. The following packages are not installed through composer:'))], + ], + // Dependency drupal/new_dependency of type 'drupal-library' will not show + // up in the error because it is not one of the covered types + // ('drupal-module', 'drupal-theme' or 'drupal-profile'). Module + // new_module1 will also not show up as it's name doesn't start with + // 'drupal/'. + // @see \Drupal\automatic_updates_extensions\Validator\PackagesInstalledWithComposerValidator + 'module_theme_profile_dependency_not_installed_via_composer' => [ + "$fixtures_folder/module_theme_profile_dependency_not_installed.staged.installed.json", + [ + ValidationResult::createError( + ['new_module', 'new_theme', 'new_profile'], + t('Automatic Updates can only update projects that were installed via Composer. The following packages are not installed through composer:')), + ], + ], + ]; + } + + /** + * Tests the packages installed with composer during pre-apply. + * + * @param string $staged_installed + * Path of `staged.installed.json` file. It will be used as the virtual + * project's staged `vendor/composer/installed.json` file. + * @param array $expected_results + * The expected validation results. + * + * @dataProvider providerPreApplyException + */ + public function testPreApplyException(string $staged_installed, array $expected_results): void { + // Path of `active.installed.json` file. It will be used as the virtual + // project's active `vendor/composer/installed.json` file. + $active_installed = __DIR__ . '/../../../fixtures/packages_installed_with_composer_validator/active.installed.json'; + $this->assertFileIsReadable($active_installed); + $this->assertFileIsReadable($staged_installed); + copy($active_installed, "$this->activeDir/vendor/composer/installed.json"); + $listener = function (PreApplyEvent $event) use ($staged_installed): void { + $stage_dir = $event->getStage()->getStageDirectory(); + copy($staged_installed, $stage_dir . "/vendor/composer/installed.json"); + }; + $this->container->get('event_dispatcher')->addListener(PreApplyEvent::class, $listener, 1000); + $this->assertUpdateResults([], $expected_results, PreApplyEvent::class); + } + +} diff --git a/automatic_updates_extensions/tests/src/Kernel/Validator/UpdateReleaseValidatorTest.php b/automatic_updates_extensions/tests/src/Kernel/Validator/UpdateReleaseValidatorTest.php index eea42e80b0..ab79f25491 100644 --- a/automatic_updates_extensions/tests/src/Kernel/Validator/UpdateReleaseValidatorTest.php +++ b/automatic_updates_extensions/tests/src/Kernel/Validator/UpdateReleaseValidatorTest.php @@ -14,6 +14,14 @@ use Drupal\Tests\automatic_updates_extensions\Kernel\AutomaticUpdatesExtensionsK */ class UpdateReleaseValidatorTest extends AutomaticUpdatesExtensionsKernelTestBase { + /** + * {@inheritdoc} + */ + protected function setUp(): void { + $this->disableValidators[] = 'automatic_updates_extensions.validator.packages_installed_with_composer'; + parent::setUp(); + } + /** * Tests updating to a release. * diff --git a/automatic_updates_extensions/tests/src/Traits/FormTestTrait.php b/automatic_updates_extensions/tests/src/Traits/FormTestTrait.php index 33d4bd1005..3b12fa8c95 100644 --- a/automatic_updates_extensions/tests/src/Traits/FormTestTrait.php +++ b/automatic_updates_extensions/tests/src/Traits/FormTestTrait.php @@ -20,12 +20,14 @@ trait FormTestTrait { * The expected installed version. * @param string $expected_target_version * The expected target version. + * @param int $row + * The row number. */ - private function assertUpdateTableRow(WebAssert $assert, string $expected_project_title, string $expected_installed_version, string $expected_target_version): void { - $assert->elementTextContains('css', '.update-recommended td:nth-of-type(2)', $expected_project_title); - $assert->elementTextContains('css', '.update-recommended td:nth-of-type(3)', $expected_installed_version); - $assert->elementTextContains('css', '.update-recommended td:nth-of-type(4)', $expected_target_version); - $assert->elementsCount('css', '.update-recommended tbody tr', 1); + private function assertUpdateTableRow(WebAssert $assert, string $expected_project_title, string $expected_installed_version, string $expected_target_version, int $row = 1): void { + $row_selector = ".update-recommended tr:nth-of-type($row)"; + $assert->elementTextContains('css', $row_selector . ' td:nth-of-type(2)', $expected_project_title); + $assert->elementTextContains('css', $row_selector . ' td:nth-of-type(3)', $expected_installed_version); + $assert->elementTextContains('css', $row_selector . ' td:nth-of-type(4)', $expected_target_version); } } -- GitLab