From d90ebb937aae83c04c2d2317b3dbaeb7b892646b Mon Sep 17 00:00:00 2001 From: phenaproxima <phenaproxima@205645.no-reply.drupal.org> Date: Fri, 20 May 2022 14:26:52 +0000 Subject: [PATCH] Issue #3280180 by phenaproxima, tedbow: Consolidate all version validation logic into a single class --- automatic_updates.services.yml | 32 +- src/CronUpdater.php | 2 +- src/Form/UpdaterForm.php | 4 +- src/ReleaseChooser.php | 48 ++- src/Validator/CronUpdateVersionValidator.php | 124 ------ src/Validator/InstalledVersionValidator.php | 59 --- src/Validator/UpdateReleaseValidator.php | 60 --- src/Validator/UpdateVersionValidator.php | 215 ---------- .../VersionPolicy/ForbidDevSnapshot.php | 43 ++ .../VersionPolicy/ForbidDowngrade.php | 43 ++ .../VersionPolicy/ForbidMinorUpdates.php | 47 +++ .../VersionPolicy/MajorVersionMatch.php | 48 +++ .../VersionPolicy/MinorUpdatesEnabled.php | 86 ++++ .../VersionPolicy/StableReleaseInstalled.php | 43 ++ .../VersionPolicy/TargetSecurityRelease.php | 44 ++ .../TargetVersionInstallable.php | 45 +++ .../VersionPolicy/TargetVersionStable.php | 45 +++ src/Validator/VersionPolicyValidator.php | 259 ++++++++++++ src/VersionParsingTrait.php | 15 + .../drupal.9.8.2-older-sec-release.xml | 13 + .../Kernel/AutomaticUpdatesKernelTestBase.php | 4 +- tests/src/Kernel/ProjectInfoTest.php | 2 +- .../CronUpdateVersionValidatorTest.php | 77 ---- .../InstalledVersionValidatorTest.php | 38 -- .../UpdateReleaseValidatorTest.php | 40 -- .../UpdateVersionValidatorTest.php | 332 --------------- .../VersionPolicyValidatorTest.php | 377 ++++++++++++++++++ tests/src/Kernel/ReleaseChooserTest.php | 42 +- tests/src/Traits/VersionPolicyTestTrait.php | 32 ++ .../VersionPolicy/ForbidDevSnapshotTest.php | 64 +++ .../VersionPolicy/ForbidDowngradeTest.php | 66 +++ .../VersionPolicy/ForbidMinorUpdatesTest.php | 91 +++++ .../VersionPolicy/MajorVersionMatchTest.php | 81 ++++ .../VersionPolicy/MinorUpdatesEnabledTest.php | 137 +++++++ .../StableReleaseInstalledTest.php | 60 +++ .../TargetSecurityReleaseTest.php | 68 ++++ .../TargetVersionInstallableTest.php | 69 ++++ .../VersionPolicy/TargetVersionStableTest.php | 64 +++ 38 files changed, 1905 insertions(+), 1014 deletions(-) delete mode 100644 src/Validator/CronUpdateVersionValidator.php delete mode 100644 src/Validator/InstalledVersionValidator.php delete mode 100644 src/Validator/UpdateReleaseValidator.php delete mode 100644 src/Validator/UpdateVersionValidator.php create mode 100644 src/Validator/VersionPolicy/ForbidDevSnapshot.php create mode 100644 src/Validator/VersionPolicy/ForbidDowngrade.php create mode 100644 src/Validator/VersionPolicy/ForbidMinorUpdates.php create mode 100644 src/Validator/VersionPolicy/MajorVersionMatch.php create mode 100644 src/Validator/VersionPolicy/MinorUpdatesEnabled.php create mode 100644 src/Validator/VersionPolicy/StableReleaseInstalled.php create mode 100644 src/Validator/VersionPolicy/TargetSecurityRelease.php create mode 100644 src/Validator/VersionPolicy/TargetVersionInstallable.php create mode 100644 src/Validator/VersionPolicy/TargetVersionStable.php create mode 100644 src/Validator/VersionPolicyValidator.php delete mode 100644 tests/src/Kernel/ReadinessValidation/CronUpdateVersionValidatorTest.php delete mode 100644 tests/src/Kernel/ReadinessValidation/InstalledVersionValidatorTest.php delete mode 100644 tests/src/Kernel/ReadinessValidation/UpdateReleaseValidatorTest.php delete mode 100644 tests/src/Kernel/ReadinessValidation/UpdateVersionValidatorTest.php create mode 100644 tests/src/Kernel/ReadinessValidation/VersionPolicyValidatorTest.php create mode 100644 tests/src/Traits/VersionPolicyTestTrait.php create mode 100644 tests/src/Unit/VersionPolicy/ForbidDevSnapshotTest.php create mode 100644 tests/src/Unit/VersionPolicy/ForbidDowngradeTest.php create mode 100644 tests/src/Unit/VersionPolicy/ForbidMinorUpdatesTest.php create mode 100644 tests/src/Unit/VersionPolicy/MajorVersionMatchTest.php create mode 100644 tests/src/Unit/VersionPolicy/MinorUpdatesEnabledTest.php create mode 100644 tests/src/Unit/VersionPolicy/StableReleaseInstalledTest.php create mode 100644 tests/src/Unit/VersionPolicy/TargetSecurityReleaseTest.php create mode 100644 tests/src/Unit/VersionPolicy/TargetVersionInstallableTest.php create mode 100644 tests/src/Unit/VersionPolicy/TargetVersionStableTest.php diff --git a/automatic_updates.services.yml b/automatic_updates.services.yml index bf3916ff1c..a5e5545c3a 100644 --- a/automatic_updates.services.yml +++ b/automatic_updates.services.yml @@ -29,7 +29,7 @@ services: automatic_updates.cron_updater: class: Drupal\automatic_updates\CronUpdater arguments: - - '@automatic_updates.cron_release_chooser' + - '@automatic_updates.release_chooser' - '@logger.factory' - '@config.factory' - '@package_manager.path_locator' @@ -52,28 +52,10 @@ services: - '@string_translation' tags: - { name: event_subscriber } - automatic_updates.update_version_validator: - class: Drupal\automatic_updates\Validator\UpdateVersionValidator - arguments: - - '@string_translation' - - '@config.factory' - tags: - - { name: event_subscriber } - automatic_updates.cron_update_version_validator: - class: Drupal\automatic_updates\Validator\CronUpdateVersionValidator - arguments: - - '@string_translation' - - '@config.factory' - tags: - - { name: event_subscriber } automatic_updates.release_chooser: class: Drupal\automatic_updates\ReleaseChooser arguments: - - '@automatic_updates.update_version_validator' - automatic_updates.cron_release_chooser: - class: Drupal\automatic_updates\ReleaseChooser - arguments: - - '@automatic_updates.cron_update_version_validator' + - '@automatic_updates.validator.version_policy' automatic_updates.composer_executable_validator: class: Drupal\automatic_updates\Validator\PackageManagerReadinessCheck arguments: @@ -133,12 +115,10 @@ services: class: Drupal\automatic_updates\Validator\XdebugValidator tags: - { name: event_subscriber } - automatic_updates.validator.installed_version: - class: Drupal\automatic_updates\Validator\InstalledVersionValidator - tags: - - { name: event_subscriber } - automatic_updates.validator.target_release: - class: Drupal\automatic_updates\Validator\UpdateReleaseValidator + automatic_updates.validator.version_policy: + class: Drupal\automatic_updates\Validator\VersionPolicyValidator + arguments: + - '@class_resolver' tags: - { name: event_subscriber } automatic_updates.config_subscriber: diff --git a/src/CronUpdater.php b/src/CronUpdater.php index e6a9dc4573..6f07e0255e 100644 --- a/src/CronUpdater.php +++ b/src/CronUpdater.php @@ -87,7 +87,7 @@ class CronUpdater extends Updater { return; } - $next_release = $this->releaseChooser->getLatestInInstalledMinor(); + $next_release = $this->releaseChooser->getLatestInInstalledMinor($this); if ($next_release) { $this->performUpdate($next_release->getVersion(), $timeout); } diff --git a/src/Form/UpdaterForm.php b/src/Form/UpdaterForm.php index 0de0dd7309..c44d057d91 100644 --- a/src/Form/UpdaterForm.php +++ b/src/Form/UpdaterForm.php @@ -139,9 +139,9 @@ class UpdaterForm extends FormBase { // one release on the form. First, try to show the latest release in the // currently installed minor. Failing that, try to show the latest // release in the next minor. - $recommended_release = $this->releaseChooser->getLatestInInstalledMinor(); + $recommended_release = $this->releaseChooser->getLatestInInstalledMinor($this->updater); if (!$recommended_release) { - $recommended_release = $this->releaseChooser->getLatestInNextMinor(); + $recommended_release = $this->releaseChooser->getLatestInNextMinor($this->updater); } } catch (\RuntimeException $e) { diff --git a/src/ReleaseChooser.php b/src/ReleaseChooser.php index 2725bab4c5..0aad2c2c60 100644 --- a/src/ReleaseChooser.php +++ b/src/ReleaseChooser.php @@ -3,7 +3,7 @@ namespace Drupal\automatic_updates; use Composer\Semver\Semver; -use Drupal\automatic_updates\Validator\UpdateVersionValidator; +use Drupal\automatic_updates\Validator\VersionPolicyValidator; use Drupal\automatic_updates_9_3_shim\ProjectRelease; use Drupal\Core\Extension\ExtensionVersion; @@ -15,11 +15,11 @@ class ReleaseChooser { use VersionParsingTrait; /** - * The version validator service. + * The version policy validator service. * - * @var \Drupal\automatic_updates\Validator\UpdateVersionValidator + * @var \Drupal\automatic_updates\Validator\VersionPolicyValidator */ - protected $versionValidator; + protected $versionPolicyValidator; /** * The project information fetcher. @@ -31,25 +31,31 @@ class ReleaseChooser { /** * Constructs an ReleaseChooser object. * - * @param \Drupal\automatic_updates\Validator\UpdateVersionValidator $version_validator + * @param \Drupal\automatic_updates\Validator\VersionPolicyValidator $version_policy_validator * The version validator. */ - public function __construct(UpdateVersionValidator $version_validator) { - $this->versionValidator = $version_validator; + public function __construct(VersionPolicyValidator $version_policy_validator) { + $this->versionPolicyValidator = $version_policy_validator; $this->projectInfo = new ProjectInfo('drupal'); } /** * Returns the releases that are installable. * + * @param \Drupal\automatic_updates\Updater $updater + * The updater that will be used to install the releases. + * * @return \Drupal\automatic_updates_9_3_shim\ProjectRelease[] - * The releases that are installable according to the version validator - * service. + * The releases that are installable by the given updtaer, according to the + * version validator service. */ - protected function getInstallableReleases(): array { + protected function getInstallableReleases(Updater $updater): array { + $filter = function (string $version) use ($updater): bool { + return empty($this->versionPolicyValidator->validateVersion($updater, $version)); + }; return array_filter( $this->projectInfo->getInstallableReleases(), - [$this->versionValidator, 'isValidVersion'], + $filter, ARRAY_FILTER_USE_KEY ); } @@ -57,6 +63,8 @@ class ReleaseChooser { /** * Gets the most recent release in the same minor as a specified version. * + * @param \Drupal\automatic_updates\Updater $updater + * The updater that will be used to install the release. * @param string $version * The full semantic version number, which must include a patch version. * @@ -66,11 +74,11 @@ class ReleaseChooser { * @throws \InvalidArgumentException * If the given semantic version number does not contain a patch version. */ - protected function getMostRecentReleaseInMinor(string $version): ?ProjectRelease { + protected function getMostRecentReleaseInMinor(Updater $updater, string $version): ?ProjectRelease { if (static::getPatchVersion($version) === NULL) { throw new \InvalidArgumentException("The version number $version does not contain a patch version"); } - $releases = $this->getInstallableReleases(); + $releases = $this->getInstallableReleases($updater); foreach ($releases as $release) { // Checks if the release is in the same minor as the currently installed // version. For example, if the current version is 9.8.0 then the @@ -99,12 +107,15 @@ class ReleaseChooser { * This will only return a release if it passes the ::isValidVersion() method * of the version validator service injected into this class. * + * @param \Drupal\automatic_updates\Updater $updater + * The updater which will install the release. + * * @return \Drupal\automatic_updates_9_3_shim\ProjectRelease|null * The latest release in the currently installed minor, if any, otherwise * NULL. */ - public function getLatestInInstalledMinor(): ?ProjectRelease { - return $this->getMostRecentReleaseInMinor($this->getInstalledVersion()); + public function getLatestInInstalledMinor(Updater $updater): ?ProjectRelease { + return $this->getMostRecentReleaseInMinor($updater, $this->getInstalledVersion()); } /** @@ -113,13 +124,16 @@ class ReleaseChooser { * This will only return a release if it passes the ::isValidVersion() method * of the version validator service injected into this class. * + * @param \Drupal\automatic_updates\Updater $updater + * The updater which will install the release. + * * @return \Drupal\automatic_updates_9_3_shim\ProjectRelease|null * The latest release in the next minor, if any, otherwise NULL. */ - public function getLatestInNextMinor(): ?ProjectRelease { + public function getLatestInNextMinor(Updater $updater): ?ProjectRelease { $installed_version = ExtensionVersion::createFromVersionString($this->getInstalledVersion()); $next_minor = $installed_version->getMajorVersion() . '.' . (((int) $installed_version->getMinorVersion()) + 1) . '.0'; - return $this->getMostRecentReleaseInMinor($next_minor); + return $this->getMostRecentReleaseInMinor($updater, $next_minor); } } diff --git a/src/Validator/CronUpdateVersionValidator.php b/src/Validator/CronUpdateVersionValidator.php deleted file mode 100644 index 0cf8c3b183..0000000000 --- a/src/Validator/CronUpdateVersionValidator.php +++ /dev/null @@ -1,124 +0,0 @@ -<?php - -namespace Drupal\automatic_updates\Validator; - -use Composer\Semver\Semver; -use Drupal\automatic_updates\CronUpdater; -use Drupal\automatic_updates\ProjectInfo; -use Drupal\automatic_updates\VersionParsingTrait; -use Drupal\Core\Extension\ExtensionVersion; -use Drupal\package_manager\Stage; -use Drupal\package_manager\ValidationResult; - -/** - * Validates the target version of Drupal core before a cron update. - * - * @internal - * This class is an internal part of the module's cron update handling and - * should not be used by external code. - */ -final class CronUpdateVersionValidator extends UpdateVersionValidator { - - use VersionParsingTrait; - - /** - * {@inheritdoc} - */ - protected static function isStageSupported(Stage $stage): bool { - // @todo Add test coverage for the call to getMode() in - // https://www.drupal.org/i/3276662. - return $stage instanceof CronUpdater && $stage->getMode() !== CronUpdater::DISABLED; - } - - /** - * {@inheritdoc} - */ - protected function getNextPossibleUpdateVersion(): ?string { - $project_info = new ProjectInfo('drupal'); - $installed_version = $project_info->getInstalledVersion(); - if ($possible_releases = $project_info->getInstallableReleases()) { - // The next possible update version for cron should be the lowest possible - // release. - $possible_release = array_pop($possible_releases); - if (Semver::satisfies($possible_release->getVersion(), "~$installed_version")) { - return $possible_release->getVersion(); - } - } - return NULL; - } - - /** - * {@inheritdoc} - */ - public function getValidationResult(string $to_version_string): ?ValidationResult { - if ($result = parent::getValidationResult($to_version_string)) { - return $result; - } - $from_version_string = $this->getCoreVersion(); - $to_version = ExtensionVersion::createFromVersionString($to_version_string); - $from_version = ExtensionVersion::createFromVersionString($from_version_string); - $variables = [ - '@to_version' => $to_version_string, - '@from_version' => $from_version_string, - ]; - // @todo Return multiple validation messages and summary in - // https://www.drupal.org/project/automatic_updates/issues/3272068. - // Validate that both the from and to versions are stable releases. - if ($from_version->getVersionExtra()) { - return ValidationResult::createError([ - $this->t('Drupal cannot be automatically updated during cron from its current version, @from_version, because Automatic Updates only supports updating from stable versions during cron.', $variables), - ]); - } - if ($to_version->getVersionExtra()) { - // Because we do not support updating to a new minor version during - // cron it is probably impossible to update from a stable version to - // a unstable/pre-release version, but we should check this condition - // just in case. - return ValidationResult::createError([ - $this->t('Drupal cannot be automatically updated during cron to the recommended version, @to_version, because Automatic Updates only supports updating to stable versions during cron.', $variables), - ]); - } - - if ($from_version->getMinorVersion() !== $to_version->getMinorVersion()) { - return ValidationResult::createError([ - $this->t('Drupal cannot be automatically updated from its current version, @from_version, to the recommended version, @to_version, because automatic updates from one minor version to another are not supported during cron.', $variables), - ]); - } - - // Only updating to the next patch release is supported during cron. - $supported_patch_version = $from_version->getMajorVersion() . '.' . $from_version->getMinorVersion() . '.' . (((int) static::getPatchVersion($from_version_string)) + 1); - if ($to_version_string !== $supported_patch_version) { - return ValidationResult::createError([ - $this->t('Drupal cannot be automatically updated during cron from its current version, @from_version, to the recommended version, @to_version, because Automatic Updates only supports 1 patch version update during cron.', $variables), - ]); - } - - // We cannot use dependency injection to get the cron updater because that - // would create a circular service dependency. - $level = \Drupal::service('automatic_updates.cron_updater') - ->getMode(); - - // If both the from and to version numbers are valid check if the current - // settings only allow security updates during cron and if so ensure the - // update release is a security release. - if ($level === CronUpdater::SECURITY) { - $releases = (new ProjectInfo('drupal'))->getInstallableReleases(); - // @todo Remove this check and add validation to - // \Drupal\automatic_updates\Validator\UpdateVersionValidator::getValidationResult() - // to ensure the update release is always secure and supported in - // https://www.drupal.org/i/3271468. - if (!isset($releases[$to_version_string])) { - return ValidationResult::createError([ - $this->t('Drupal cannot be automatically updated during cron from its current version, @from_version, to the recommended version, @to_version, because @to_version is not a valid release.', $variables), - ]); - } - if (!$releases[$to_version_string]->isSecurityRelease()) { - return ValidationResult::createError([ - $this->t('Drupal cannot be automatically updated during cron from its current version, @from_version, to the recommended version, @to_version, because @to_version is not a security release.', $variables), - ]); - } - } - return NULL; - } - -} diff --git a/src/Validator/InstalledVersionValidator.php b/src/Validator/InstalledVersionValidator.php deleted file mode 100644 index 0a827de58b..0000000000 --- a/src/Validator/InstalledVersionValidator.php +++ /dev/null @@ -1,59 +0,0 @@ -<?php - -namespace Drupal\automatic_updates\Validator; - -use Drupal\automatic_updates\Event\ReadinessCheckEvent; -use Drupal\automatic_updates\ProjectInfo; -use Drupal\automatic_updates\Updater; -use Drupal\Core\Extension\ExtensionVersion; -use Drupal\Core\StringTranslation\StringTranslationTrait; -use Drupal\package_manager\Event\PreCreateEvent; -use Drupal\package_manager\Event\PreOperationStageEvent; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; - -/** - * Validates that the site can update from the installed version of Drupal. - * - * @internal - * This class is an internal part of the module's update handling and - * should not be used by external code. - */ -class InstalledVersionValidator implements EventSubscriberInterface { - - use StringTranslationTrait; - - /** - * Checks that the site can update from the installed version of Drupal. - * - * @param \Drupal\package_manager\Event\PreOperationStageEvent $event - * The event object. - */ - public function checkInstalledVersion(PreOperationStageEvent $event): void { - // This check only works with Automatic Updates. - if (!$event->getStage() instanceof Updater) { - return; - } - - $installed_version = (new ProjectInfo('drupal'))->getInstalledVersion(); - $extra = ExtensionVersion::createFromVersionString($installed_version) - ->getVersionExtra(); - - if ($extra === 'dev') { - $message = $this->t('Drupal cannot be automatically updated from the installed version, @installed_version, because automatic updates from a dev version to any other version are not supported.', [ - '@installed_version' => $installed_version, - ]); - $event->addError([$message]); - } - } - - /** - * {@inheritdoc} - */ - public static function getSubscribedEvents() { - return [ - ReadinessCheckEvent::class => 'checkInstalledVersion', - PreCreateEvent::class => 'checkInstalledVersion', - ]; - } - -} diff --git a/src/Validator/UpdateReleaseValidator.php b/src/Validator/UpdateReleaseValidator.php deleted file mode 100644 index 97449b36d3..0000000000 --- a/src/Validator/UpdateReleaseValidator.php +++ /dev/null @@ -1,60 +0,0 @@ -<?php - -namespace Drupal\automatic_updates\Validator; - -use Drupal\automatic_updates\ProjectInfo; -use Drupal\automatic_updates\Updater; -use Drupal\Core\StringTranslation\StringTranslationTrait; -use Drupal\package_manager\Event\PreCreateEvent; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; - -/** - * Validates that the target release of Drupal core is secure and supported. - * - * @internal - * This class is an internal part of the module's update handling and - * should not be used by external code. - */ -class UpdateReleaseValidator implements EventSubscriberInterface { - - use StringTranslationTrait; - - /** - * Checks that the target version of Drupal core is secure and supported. - * - * @param \Drupal\package_manager\Event\PreCreateEvent $event - * The event object. - */ - public function checkRelease(PreCreateEvent $event): void { - $stage = $event->getStage(); - // This check only works with Automatic Updates. - if (!$stage instanceof Updater) { - return; - } - - $package_versions = $stage->getPackageVersions(); - // The updater will only update Drupal core, so all production dependencies - // will be Drupal core packages. - $target_version = reset($package_versions['production']); - - // If the target version isn't in the list of installable releases, then it - // isn't secure and supported and we should flag an error. - $releases = (new ProjectInfo('drupal'))->getInstallableReleases(); - if (empty($releases) || !array_key_exists($target_version, $releases)) { - $message = $this->t('Cannot update Drupal core to @target_version because it is not in the list of installable releases.', [ - '@target_version' => $target_version, - ]); - $event->addError([$message]); - } - } - - /** - * {@inheritdoc} - */ - public static function getSubscribedEvents() { - return [ - PreCreateEvent::class => 'checkRelease', - ]; - } - -} diff --git a/src/Validator/UpdateVersionValidator.php b/src/Validator/UpdateVersionValidator.php deleted file mode 100644 index bd87af4ab8..0000000000 --- a/src/Validator/UpdateVersionValidator.php +++ /dev/null @@ -1,215 +0,0 @@ -<?php - -namespace Drupal\automatic_updates\Validator; - -use Composer\Semver\Comparator; -use Composer\Semver\Semver; -use Drupal\automatic_updates\CronUpdater; -use Drupal\automatic_updates\Event\ReadinessCheckEvent; -use Drupal\automatic_updates\ProjectInfo; -use Drupal\automatic_updates\Updater; -use Drupal\Core\Config\ConfigFactoryInterface; -use Drupal\Core\Extension\ExtensionVersion; -use Drupal\Core\StringTranslation\StringTranslationTrait; -use Drupal\Core\StringTranslation\TranslationInterface; -use Drupal\package_manager\Event\PreCreateEvent; -use Drupal\package_manager\Event\PreOperationStageEvent; -use Drupal\package_manager\Event\StageEvent; -use Drupal\package_manager\Stage; -use Drupal\package_manager\ValidationResult; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; - -/** - * Validates that core updates are within a supported version range. - * - * @internal - * This class is an internal part of the module's update handling and - * should not be used by external code. - */ -class UpdateVersionValidator implements EventSubscriberInterface { - - use StringTranslationTrait; - - /** - * The config factory service. - * - * @var \Drupal\Core\Config\ConfigFactoryInterface - */ - protected $configFactory; - - /** - * Constructs a UpdateVersionValidation object. - * - * @param \Drupal\Core\StringTranslation\TranslationInterface $translation - * The translation service. - * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory - * The config factory service. - */ - public function __construct(TranslationInterface $translation, ConfigFactoryInterface $config_factory) { - $this->setStringTranslation($translation); - $this->configFactory = $config_factory; - } - - /** - * Returns the running core version, according to the Update module. - * - * @return string - * The running core version as known to the Update module. - */ - protected function getCoreVersion(): string { - return (new ProjectInfo('drupal'))->getInstalledVersion(); - } - - /** - * Validates that core is being updated within an allowed version range. - * - * @param \Drupal\package_manager\Event\PreOperationStageEvent $event - * The event object. - */ - public function checkUpdateVersion(PreOperationStageEvent $event): void { - if (!static::isStageSupported($event->getStage())) { - return; - } - if ($to_version = $this->getUpdateVersion($event)) { - if ($result = $this->getValidationResult($to_version)) { - $event->addError($result->getMessages(), $result->getSummary()); - } - } - } - - /** - * {@inheritdoc} - */ - public static function getSubscribedEvents() { - return [ - PreCreateEvent::class => 'checkUpdateVersion', - ReadinessCheckEvent::class => 'checkUpdateVersion', - ]; - } - - /** - * Gets the update version. - * - * @param \Drupal\package_manager\Event\StageEvent $event - * The event. - * - * @return string|null - * The version that the site will update to if any, otherwise NULL. - */ - protected function getUpdateVersion(StageEvent $event): ?string { - /** @var \Drupal\automatic_updates\Updater $updater */ - $updater = $event->getStage(); - if ($event instanceof ReadinessCheckEvent) { - $package_versions = $event->getPackageVersions(); - if (!$package_versions) { - // During readiness checks we might not have a version to update to. - // Use the next possible update version to run checks against. - return $this->getNextPossibleUpdateVersion(); - } - } - else { - // If the stage has begun its life cycle, we expect it knows the desired - // package versions. - $package_versions = $updater->getPackageVersions()['production']; - } - if ($package_versions) { - // All the core packages will be updated to the same version, so it - // doesn't matter which specific package we're looking at. - $core_package_name = key($updater->getActiveComposer()->getCorePackages()); - return $package_versions[$core_package_name]; - } - return NULL; - } - - /** - * Gets the next possible update version, if any. - * - * @return string|null - * The next possible update version if available, otherwise NULL. - */ - protected function getNextPossibleUpdateVersion(): ?string { - $project_info = new ProjectInfo('drupal'); - $installed_version = $project_info->getInstalledVersion(); - if ($possible_releases = $project_info->getInstallableReleases()) { - foreach ($possible_releases as $possible_release) { - $possible_version = $possible_release->getVersion(); - if (Semver::satisfies($possible_release->getVersion(), "~$installed_version")) { - return $possible_version; - } - } - } - return NULL; - } - - /** - * Determines if a version is valid. - * - * @param string $version - * The version string. - * - * @return bool - * TRUE if the version is valid (i.e., the site can update to it), otherwise - * FALSE. - */ - public function isValidVersion(string $version): bool { - return empty($this->getValidationResult($version)); - } - - /** - * Validates if an update to a specific version is allowed. - * - * @param string $to_version_string - * The version to update to. - * - * @return \Drupal\package_manager\ValidationResult|null - * NULL if the update is allowed, otherwise returns a validation result with - * the reason why the update is not allowed. - */ - protected function getValidationResult(string $to_version_string): ?ValidationResult { - $from_version_string = $this->getCoreVersion(); - $variables = [ - '@to_version' => $to_version_string, - '@from_version' => $from_version_string, - ]; - $from_version = ExtensionVersion::createFromVersionString($from_version_string); - - // @todo Return multiple validation messages and summary in - // https://www.drupal.org/project/automatic_updates/issues/3272068. - if (Comparator::lessThan($to_version_string, $from_version_string)) { - return ValidationResult::createError([ - $this->t('Update version @to_version is lower than @from_version, downgrading is not supported.', $variables), - ]); - } - $to_version = ExtensionVersion::createFromVersionString($to_version_string); - if ($from_version->getMajorVersion() !== $to_version->getMajorVersion()) { - return ValidationResult::createError([ - $this->t('Drupal cannot be automatically updated from its current version, @from_version, to the recommended version, @to_version, because automatic updates from one major version to another are not supported.', $variables), - ]); - } - if ($from_version->getMinorVersion() !== $to_version->getMinorVersion()) { - if (!$this->configFactory->get('automatic_updates.settings')->get('allow_core_minor_updates')) { - return ValidationResult::createError([ - $this->t('Drupal cannot be automatically updated from its current version, @from_version, to the recommended version, @to_version, because automatic updates from one minor version to another are not supported.', $variables), - ]); - } - } - return NULL; - } - - /** - * Determines if a stage is supported by this validator. - * - * @param \Drupal\package_manager\Stage $stage - * The stage to check. - * - * @return bool - * TRUE if the stage is supported by this validator, otherwise FALSE. - */ - protected static function isStageSupported(Stage $stage): bool { - // We only want to do this check if the stage belongs to Automatic Updates, - // and it is not a cron update. - // @see \Drupal\automatic_updates\Validator\CronUpdateVersionValidator - return $stage instanceof Updater && !$stage instanceof CronUpdater; - } - -} diff --git a/src/Validator/VersionPolicy/ForbidDevSnapshot.php b/src/Validator/VersionPolicy/ForbidDevSnapshot.php new file mode 100644 index 0000000000..ca6842a650 --- /dev/null +++ b/src/Validator/VersionPolicy/ForbidDevSnapshot.php @@ -0,0 +1,43 @@ +<?php + +namespace Drupal\automatic_updates\Validator\VersionPolicy; + +use Drupal\Core\Extension\ExtensionVersion; +use Drupal\Core\StringTranslation\StringTranslationTrait; + +/** + * A policy rule that forbids updating from a dev snapshot. + * + * @internal + * This is an internal part of Automatic Updates' version policy for + * Drupal core. It may be changed or removed at any time without warning. + * External code should not interact with this class. + */ +class ForbidDevSnapshot { + + use StringTranslationTrait; + + /** + * Checks if the installed version of Drupal is a dev snapshot. + * + * @param string $installed_version + * The installed version of Drupal. + * + * @return \Drupal\Core\StringTranslation\TranslatableMarkup[] + * The error messages, if any. + */ + public function validate(string $installed_version): array { + $extra = ExtensionVersion::createFromVersionString($installed_version) + ->getVersionExtra(); + + if ($extra === 'dev') { + return [ + $this->t('Drupal cannot be automatically updated from the installed version, @installed_version, because automatic updates from a dev version to any other version are not supported.', [ + '@installed_version' => $installed_version, + ]), + ]; + } + return []; + } + +} diff --git a/src/Validator/VersionPolicy/ForbidDowngrade.php b/src/Validator/VersionPolicy/ForbidDowngrade.php new file mode 100644 index 0000000000..93f05e0fc9 --- /dev/null +++ b/src/Validator/VersionPolicy/ForbidDowngrade.php @@ -0,0 +1,43 @@ +<?php + +namespace Drupal\automatic_updates\Validator\VersionPolicy; + +use Composer\Semver\Comparator; +use Drupal\Core\StringTranslation\StringTranslationTrait; + +/** + * A policy rule that forbids downgrading. + * + * @internal + * This is an internal part of Automatic Updates' version policy for + * Drupal core. It may be changed or removed at any time without warning. + * External code should not interact with this class. + */ +class ForbidDowngrade { + + use StringTranslationTrait; + + /** + * Checks if the target version of Drupal is older than the installed version. + * + * @param string $installed_version + * The installed version of Drupal. + * @param string|null $target_version + * The target version of Drupal core, or NULL if not known. + * + * @return \Drupal\Core\StringTranslation\TranslatableMarkup[] + * The error messages, if any. + */ + public function validate(string $installed_version, ?string $target_version): array { + if (Comparator::lessThan($target_version, $installed_version)) { + return [ + $this->t('Update version @target_version is lower than @installed_version, downgrading is not supported.', [ + '@target_version' => $target_version, + '@installed_version' => $installed_version, + ]), + ]; + } + return []; + } + +} diff --git a/src/Validator/VersionPolicy/ForbidMinorUpdates.php b/src/Validator/VersionPolicy/ForbidMinorUpdates.php new file mode 100644 index 0000000000..86327d4161 --- /dev/null +++ b/src/Validator/VersionPolicy/ForbidMinorUpdates.php @@ -0,0 +1,47 @@ +<?php + +namespace Drupal\automatic_updates\Validator\VersionPolicy; + +use Drupal\automatic_updates\VersionParsingTrait; +use Drupal\Core\StringTranslation\StringTranslationTrait; + +/** + * A policy rule forbidding minor updates during cron. + * + * @internal + * This is an internal part of Automatic Updates' version policy for + * Drupal core. It may be changed or removed at any time without warning. + * External code should not interact with this class. + */ +class ForbidMinorUpdates { + + use StringTranslationTrait; + use VersionParsingTrait; + + /** + * Checks if the target minor version of Drupal is different than installed. + * + * @param string $installed_version + * The installed version of Drupal. + * @param string|null $target_version + * The target version of Drupal, or NULL if not known. + * + * @return \Drupal\Core\StringTranslation\TranslatableMarkup[] + * The error messages, if any. + */ + public function validate(string $installed_version, ?string $target_version): array { + $installed_minor = static::getMajorAndMinorVersion($installed_version); + $target_minor = static::getMajorAndMinorVersion($target_version); + + if ($installed_minor !== $target_minor) { + return [ + $this->t('Drupal cannot be automatically updated from its current version, @installed_version, to the recommended version, @target_version, because automatic updates from one minor version to another are not supported during cron.', [ + '@installed_version' => $installed_version, + '@target_version' => $target_version, + ]), + ]; + } + return []; + } + +} diff --git a/src/Validator/VersionPolicy/MajorVersionMatch.php b/src/Validator/VersionPolicy/MajorVersionMatch.php new file mode 100644 index 0000000000..58709075b7 --- /dev/null +++ b/src/Validator/VersionPolicy/MajorVersionMatch.php @@ -0,0 +1,48 @@ +<?php + +namespace Drupal\automatic_updates\Validator\VersionPolicy; + +use Drupal\Core\Extension\ExtensionVersion; +use Drupal\Core\StringTranslation\StringTranslationTrait; + +/** + * A policy rule that requires updating within the same major version. + * + * @internal + * This is an internal part of Automatic Updates' version policy for + * Drupal core. It may be changed or removed at any time without warning. + * External code should not interact with this class. + */ +class MajorVersionMatch { + + use StringTranslationTrait; + + /** + * Checks that the target version of Drupal is in the same minor as installed. + * + * @param string $installed_version + * The installed version of Drupal. + * @param string|null $target_version + * The target version of Drupal, or NULL if not known. + * + * @return \Drupal\Core\StringTranslation\TranslatableMarkup[] + * The error messages, if any. + */ + public function validate(string $installed_version, ?string $target_version): array { + $installed_major = ExtensionVersion::createFromVersionString($installed_version) + ->getMajorVersion(); + $target_major = ExtensionVersion::createFromVersionString($target_version) + ->getMajorVersion(); + + if ($installed_major !== $target_major) { + return [ + $this->t('Drupal cannot be automatically updated from its current version, @installed_version, to the recommended version, @target_version, because automatic updates from one major version to another are not supported.', [ + '@installed_version' => $installed_version, + '@target_version' => $target_version, + ]), + ]; + } + return []; + } + +} diff --git a/src/Validator/VersionPolicy/MinorUpdatesEnabled.php b/src/Validator/VersionPolicy/MinorUpdatesEnabled.php new file mode 100644 index 0000000000..52f6fd80e1 --- /dev/null +++ b/src/Validator/VersionPolicy/MinorUpdatesEnabled.php @@ -0,0 +1,86 @@ +<?php + +namespace Drupal\automatic_updates\Validator\VersionPolicy; + +use Drupal\automatic_updates\VersionParsingTrait; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * A policy rule that allows minor updates if enabled in configuration. + * + * @internal + * This is an internal part of Automatic Updates' version policy for + * Drupal core. It may be changed or removed at any time without warning. + * External code should not interact with this class. + */ +class MinorUpdatesEnabled implements ContainerInjectionInterface { + + use StringTranslationTrait; + use VersionParsingTrait; + + /** + * The config factory service. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + private $configFactory; + + /** + * Constructs a MinorUpdatesEnabled object. + * + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory service. + */ + public function __construct(ConfigFactoryInterface $config_factory) { + $this->configFactory = $config_factory; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory') + ); + } + + /** + * Checks that the target minor version of Drupal can be updated to. + * + * The update will only be allowed if the allow_core_minor_updates flag is + * set to TRUE in config. + * + * @param string $installed_version + * The installed version of Drupal. + * @param string|null $target_version + * The target version of Drupal, or NULL if not known. + * + * @return \Drupal\Core\StringTranslation\TranslatableMarkup[] + * The error messages, if any. + */ + public function validate(string $installed_version, ?string $target_version): array { + $installed_minor = static::getMajorAndMinorVersion($installed_version); + $target_minor = static::getMajorAndMinorVersion($target_version); + + if ($installed_minor === $target_minor) { + return []; + } + + $minor_updates_allowed = $this->configFactory->get('automatic_updates.settings') + ->get('allow_core_minor_updates'); + + if (!$minor_updates_allowed) { + return [ + $this->t('Drupal cannot be automatically updated from its current version, @installed_version, to the recommended version, @target_version, because automatic updates from one minor version to another are not supported.', [ + '@installed_version' => $installed_version, + '@target_version' => $target_version, + ]), + ]; + } + return []; + } + +} diff --git a/src/Validator/VersionPolicy/StableReleaseInstalled.php b/src/Validator/VersionPolicy/StableReleaseInstalled.php new file mode 100644 index 0000000000..299126d296 --- /dev/null +++ b/src/Validator/VersionPolicy/StableReleaseInstalled.php @@ -0,0 +1,43 @@ +<?php + +namespace Drupal\automatic_updates\Validator\VersionPolicy; + +use Drupal\Core\Extension\ExtensionVersion; +use Drupal\Core\StringTranslation\StringTranslationTrait; + +/** + * A policy rule that requiring the installed version to be stable. + * + * @internal + * This is an internal part of Automatic Updates' version policy for + * Drupal core. It may be changed or removed at any time without warning. + * External code should not interact with this class. + */ +class StableReleaseInstalled { + + use StringTranslationTrait; + + /** + * Checks if the installed version of Drupal is a stable release. + * + * @param string $installed_version + * The installed version of Drupal. + * + * @return \Drupal\Core\StringTranslation\TranslatableMarkup[] + * The error messages, if any. + */ + public function validate(string $installed_version): array { + $extra = ExtensionVersion::createFromVersionString($installed_version) + ->getVersionExtra(); + + if ($extra) { + return [ + $this->t('Drupal cannot be automatically updated during cron from its current version, @installed_version, because Automatic Updates only supports updating from stable versions during cron.', [ + '@installed_version' => $installed_version, + ]), + ]; + } + return []; + } + +} diff --git a/src/Validator/VersionPolicy/TargetSecurityRelease.php b/src/Validator/VersionPolicy/TargetSecurityRelease.php new file mode 100644 index 0000000000..4ac64316a1 --- /dev/null +++ b/src/Validator/VersionPolicy/TargetSecurityRelease.php @@ -0,0 +1,44 @@ +<?php + +namespace Drupal\automatic_updates\Validator\VersionPolicy; + +use Drupal\Core\StringTranslation\StringTranslationTrait; + +/** + * A policy rule requiring the target version to be a security release. + * + * @internal + * This is an internal part of Automatic Updates' version policy for + * Drupal core. It may be changed or removed at any time without warning. + * External code should not interact with this class. + */ +class TargetSecurityRelease { + + use StringTranslationTrait; + + /** + * Checks that the target version of Drupal is a security release. + * + * @param string $installed_version + * The installed version of Drupal. + * @param string|null $target_version + * The target version of Drupal, or NULL if not known. + * @param \Drupal\automatic_updates_9_3_shim\ProjectRelease[] $available_releases + * The available releases of Drupal core. + * + * @return \Drupal\Core\StringTranslation\TranslatableMarkup[] + * The error messages, if any. + */ + public function validate(string $installed_version, ?string $target_version, array $available_releases): array { + if (!$available_releases[$target_version]->isSecurityRelease()) { + return [ + $this->t('Drupal cannot be automatically updated during cron from its current version, @installed_version, to the recommended version, @target_version, because @target_version is not a security release.', [ + '@installed_version' => $installed_version, + '@target_version' => $target_version, + ]), + ]; + } + return []; + } + +} diff --git a/src/Validator/VersionPolicy/TargetVersionInstallable.php b/src/Validator/VersionPolicy/TargetVersionInstallable.php new file mode 100644 index 0000000000..de38041869 --- /dev/null +++ b/src/Validator/VersionPolicy/TargetVersionInstallable.php @@ -0,0 +1,45 @@ +<?php + +namespace Drupal\automatic_updates\Validator\VersionPolicy; + +use Drupal\Core\StringTranslation\StringTranslationTrait; + +/** + * A policy rule requiring the target version to be an installable release. + * + * @internal + * This is an internal part of Automatic Updates' version policy for + * Drupal core. It may be changed or removed at any time without warning. + * External code should not interact with this class. + */ +class TargetVersionInstallable { + + use StringTranslationTrait; + + /** + * Checks that the target version of Drupal is a known installable release. + * + * @param string $installed_version + * The installed version of Drupal. + * @param string|null $target_version + * The target version of Drupal, or NULL if not known. + * @param \Drupal\automatic_updates_9_3_shim\ProjectRelease[] $available_releases + * The available releases of Drupal core. + * + * @return \Drupal\Core\StringTranslation\TranslatableMarkup[] + * The error messages, if any. + */ + public function validate(string $installed_version, ?string $target_version, array $available_releases): array { + // If the target version isn't in the list of installable releases, we + // should flag an error. + if (empty($available_releases) || !array_key_exists($target_version, $available_releases)) { + return [ + $this->t('Cannot update Drupal core to @target_version because it is not in the list of installable releases.', [ + '@target_version' => $target_version, + ]), + ]; + } + return []; + } + +} diff --git a/src/Validator/VersionPolicy/TargetVersionStable.php b/src/Validator/VersionPolicy/TargetVersionStable.php new file mode 100644 index 0000000000..787bd477d6 --- /dev/null +++ b/src/Validator/VersionPolicy/TargetVersionStable.php @@ -0,0 +1,45 @@ +<?php + +namespace Drupal\automatic_updates\Validator\VersionPolicy; + +use Drupal\Core\Extension\ExtensionVersion; +use Drupal\Core\StringTranslation\StringTranslationTrait; + +/** + * A policy rule requiring the target version to be a stable release. + * + * @internal + * This is an internal part of Automatic Updates' version policy for + * Drupal core. It may be changed or removed at any time without warning. + * External code should not interact with this class. + */ +class TargetVersionStable { + + use StringTranslationTrait; + + /** + * Checks that the target version of Drupal is a stable release. + * + * @param string $installed_version + * The installed version of Drupal. + * @param string|null $target_version + * The target version of Drupal, or NULL if not known. + * + * @return \Drupal\Core\StringTranslation\TranslatableMarkup[] + * The error messages, if any. + */ + public function validate(string $installed_version, ?string $target_version): array { + $extra = ExtensionVersion::createFromVersionString($target_version) + ->getVersionExtra(); + + if ($extra) { + return [ + $this->t('Drupal cannot be automatically updated during cron to the recommended version, @target_version, because Automatic Updates only supports updating to stable versions during cron.', [ + '@target_version' => $target_version, + ]), + ]; + } + return []; + } + +} diff --git a/src/Validator/VersionPolicyValidator.php b/src/Validator/VersionPolicyValidator.php new file mode 100644 index 0000000000..9e5d662bdf --- /dev/null +++ b/src/Validator/VersionPolicyValidator.php @@ -0,0 +1,259 @@ +<?php + +namespace Drupal\automatic_updates\Validator; + +use Composer\Semver\Semver; +use Drupal\automatic_updates\CronUpdater; +use Drupal\automatic_updates\Event\ReadinessCheckEvent; +use Drupal\automatic_updates\ProjectInfo; +use Drupal\automatic_updates\Updater; +use Drupal\automatic_updates\Validator\VersionPolicy\ForbidDowngrade; +use Drupal\automatic_updates\Validator\VersionPolicy\ForbidMinorUpdates; +use Drupal\automatic_updates\Validator\VersionPolicy\MajorVersionMatch; +use Drupal\automatic_updates\Validator\VersionPolicy\MinorUpdatesEnabled; +use Drupal\automatic_updates\Validator\VersionPolicy\StableReleaseInstalled; +use Drupal\automatic_updates\Validator\VersionPolicy\ForbidDevSnapshot; +use Drupal\automatic_updates\Validator\VersionPolicy\TargetSecurityRelease; +use Drupal\automatic_updates\Validator\VersionPolicy\TargetVersionInstallable; +use Drupal\automatic_updates\Validator\VersionPolicy\TargetVersionStable; +use Drupal\Core\DependencyInjection\ClassResolverInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\package_manager\Event\PreCreateEvent; +use Drupal\package_manager\Event\StageEvent; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * Validates the installed and target versions of Drupal before an update. + * + * @internal + * This class is an internal part of the module's update handling and should + * not be used by external code. + */ +final class VersionPolicyValidator implements EventSubscriberInterface { + + use StringTranslationTrait; + + /** + * The class resolver service. + * + * @var \Drupal\Core\DependencyInjection\ClassResolverInterface + */ + private $classResolver; + + /** + * Constructs a VersionPolicyValidator object. + * + * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver + * The class resolver service. + */ + public function __construct(ClassResolverInterface $class_resolver) { + $this->classResolver = $class_resolver; + } + + /** + * Validates a target version of Drupal core. + * + * @param \Drupal\automatic_updates\Updater $updater + * The updater which will perform the update. + * @param string|null $target_version + * The target version of Drupal core, or NULL if it is not known. + * + * @return \Drupal\Core\StringTranslation\TranslatableMarkup[] + * The error messages returned from the first policy rule which rejected + * the given target version. + * + * @see \Drupal\automatic_updates\Validator\VersionPolicy\RuleBase::validate() + */ + public function validateVersion(Updater $updater, ?string $target_version): array { + // Check that the installed version of Drupal isn't a dev snapshot. + $rules = [ + ForbidDevSnapshot::class, + ]; + + // If the target version is known, it must conform to a few basic rules. + if ($target_version) { + // The target version must be newer than the installed version... + $rules[] = ForbidDowngrade::class; + // ...and in the same major version as the installed version... + $rules[] = MajorVersionMatch::class; + // ...and it must be a known, secure, installable release. + $rules[] = TargetVersionInstallable::class; + } + + // If this is a cron update, we may need to do additional checks. + if ($updater instanceof CronUpdater) { + $mode = $updater->getMode(); + + if ($mode !== CronUpdater::DISABLED) { + // If cron updates are enabled, the installed version must be stable; + // no alphas, betas, or RCs. + $rules[] = StableReleaseInstalled::class; + + // If the target version is known, more rules apply. + if ($target_version) { + // The target version must be stable too... + $rules[] = TargetVersionStable::class; + // ...and it must be in the same minor as the installed version. + $rules[] = ForbidMinorUpdates::class; + + // If only security updates are allowed during cron, the target + // version must be a security release. + if ($mode === CronUpdater::SECURITY) { + $rules[] = TargetSecurityRelease::class; + } + } + } + } + // If this is not a cron update, and we know the target version, minor + // version updates are allowed if configuration says so. + elseif ($target_version) { + $rules[] = MinorUpdatesEnabled::class; + } + + $installed_version = $this->getInstalledVersion(); + $available_releases = static::getAvailableReleases($updater); + + // Invoke each rule in the order that they were added to $rules, stopping + // when one returns error messages. + // @todo Return all the error messages in https://www.drupal.org/i/3281379. + foreach ($rules as $rule) { + $messages = $this->classResolver + ->getInstanceFromDefinition($rule) + ->validate($installed_version, $target_version, $available_releases); + + if ($messages) { + return $messages; + } + } + return []; + } + + /** + * Checks that the target version of Drupal is valid. + * + * @param \Drupal\package_manager\Event\StageEvent $event + * The event object. + */ + public function checkVersion(StageEvent $event): void { + $stage = $event->getStage(); + + // Only do these checks for automatic updates. + if (!$stage instanceof Updater) { + return; + } + $target_version = $this->getTargetVersion($event); + + $messages = $this->validateVersion($stage, $target_version); + if ($messages) { + $summary = $this->t('Updating from Drupal @installed_version to @target_version is not allowed.', [ + '@installed_version' => $this->getInstalledVersion(), + '@target_version' => $target_version, + ]); + $event->addError($messages, $summary); + } + } + + /** + * Returns the target version of Drupal core. + * + * @param \Drupal\package_manager\Event\StageEvent $event + * The event object. + * + * @return string|null + * The target version of Drupal core, or NULL if it could not be determined. + * + * @todo Throw an exception in certain (maybe all) situations if we cannot + * figure out the target version in https://www.drupal.org/i/3280180. + */ + private function getTargetVersion(StageEvent $event): ?string { + $updater = $event->getStage(); + + if ($event instanceof ReadinessCheckEvent) { + $package_versions = $event->getPackageVersions(); + } + else { + $package_versions = $updater->getPackageVersions()['production']; + } + + if ($package_versions) { + $core_package_name = key($updater->getActiveComposer()->getCorePackages()); + return $package_versions[$core_package_name]; + } + elseif ($event instanceof ReadinessCheckEvent) { + return $this->getTargetVersionFromAvailableReleases($updater); + } + else { + return NULL; + } + } + + /** + * Returns the target version of Drupal from the list of available releases. + * + * @param \Drupal\automatic_updates\Updater $updater + * The updater which will perform the update. + * + * @return string|null + * The target version of Drupal core, or NULL if it could not be determined. + * + * @todo Expand this doc comment to explain how the list of available releases + * is fetched, sorted, and filtered through (i.e., must match the current + * minor). Maybe reference ProjectInfo::getInstallableReleases(). + */ + private function getTargetVersionFromAvailableReleases(Updater $updater): ?string { + $installed_version = $this->getInstalledVersion(); + + foreach (self::getAvailableReleases($updater) as $possible_release) { + $possible_version = $possible_release->getVersion(); + if (Semver::satisfies($possible_version, "~$installed_version")) { + return $possible_version; + } + } + return NULL; + } + + /** + * Returns the available releases of Drupal core for a given updater. + * + * @param \Drupal\automatic_updates\Updater $updater + * The updater which will perform the update. + * + * @return \Drupal\automatic_updates_9_3_shim\ProjectRelease[] + * The available releases of Drupal core, keyed by version number and in + * descending order (i.e., newest first). Will be in ascending order (i.e., + * oldest first) if $updater is the cron updater. + * + * @see \Drupal\automatic_updates\ProjectInfo::getInstallableReleases() + */ + private static function getAvailableReleases(Updater $updater): array { + $project_info = new ProjectInfo('drupal'); + $available_releases = $project_info->getInstallableReleases() ?? []; + + if ($updater instanceof CronUpdater) { + $available_releases = array_reverse($available_releases); + } + return $available_releases; + } + + /** + * Returns the currently installed version of Drupal core. + * + * @return string|null + * The currently installed version of Drupal core, or NULL if it could not + * be determined. + */ + private function getInstalledVersion(): ?string { + return (new ProjectInfo('drupal'))->getInstalledVersion(); + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + return [ + ReadinessCheckEvent::class => 'checkVersion', + PreCreateEvent::class => 'checkVersion', + ]; + } + +} diff --git a/src/VersionParsingTrait.php b/src/VersionParsingTrait.php index c8d52f8e25..2e2a3ae5e9 100644 --- a/src/VersionParsingTrait.php +++ b/src/VersionParsingTrait.php @@ -34,4 +34,19 @@ trait VersionParsingTrait { return count($version_parts) === 3 ? $version_parts[2] : NULL; } + /** + * Returns the semantic major.minor numbers of a version string. + * + * @param string $version + * The version string. + * + * @return string + * The major.minor numbers of the version string. For example, if $version + * is 8.9.1, '8.9' will be returned. + */ + protected static function getMajorAndMinorVersion(string $version): string { + $version = ExtensionVersion::createFromVersionString($version); + return $version->getMajorVersion() . '.' . $version->getMinorVersion(); + } + } diff --git a/tests/fixtures/release-history/drupal.9.8.2-older-sec-release.xml b/tests/fixtures/release-history/drupal.9.8.2-older-sec-release.xml index 0d29b6e115..1bc31480c7 100644 --- a/tests/fixtures/release-history/drupal.9.8.2-older-sec-release.xml +++ b/tests/fixtures/release-history/drupal.9.8.2-older-sec-release.xml @@ -35,6 +35,19 @@ <term><name>Release type</name><value>Security update</value></term> </terms> </release> + <release> + <name>Drupal 9.8.1-beta1</name> + <version>9.8.1-beta1</version> + <status>published</status> + <release_link>http://example.com/drupal-9-8-1-beta1-release</release_link> + <download_link>http://example.com/drupal-9-8-1-beta1.tar.gz</download_link> + <date>1250424521</date> + <terms> + <term><name>Release type</name><value>New features</value></term> + <term><name>Release type</name><value>Bug fixes</value></term> + <term><name>Release type</name><value>Security release</value></term> + </terms> + </release> <release> <name>Drupal 9.8.0</name> <version>9.8.0</version> diff --git a/tests/src/Kernel/AutomaticUpdatesKernelTestBase.php b/tests/src/Kernel/AutomaticUpdatesKernelTestBase.php index de4baeb504..3788fb4bb4 100644 --- a/tests/src/Kernel/AutomaticUpdatesKernelTestBase.php +++ b/tests/src/Kernel/AutomaticUpdatesKernelTestBase.php @@ -65,8 +65,8 @@ abstract class AutomaticUpdatesKernelTestBase extends PackageManagerKernelTestBa // have run. $this->registerPostUpdateFunctions(); - // By default, pretend we're running Drupal core 9.8.0 and a non-security - // update to 9.8.1 is available. + // By default, pretend we're running Drupal core 9.8.1 and a non-security + // update to 9.8.2 is available. $this->setCoreVersion('9.8.1'); $this->setReleaseMetadata(['drupal' => __DIR__ . '/../../fixtures/release-history/drupal.9.8.2.xml']); diff --git a/tests/src/Kernel/ProjectInfoTest.php b/tests/src/Kernel/ProjectInfoTest.php index 7b11e703b0..e2b6c3ed41 100644 --- a/tests/src/Kernel/ProjectInfoTest.php +++ b/tests/src/Kernel/ProjectInfoTest.php @@ -92,7 +92,7 @@ class ProjectInfoTest extends AutomaticUpdatesKernelTestBase { 'core, skip insecure releases and return secure releases' => [ 'drupal.9.8.2-older-sec-release.xml', '9.7.0-alpha1', - ['9.8.2', '9.8.1', '9.8.0-alpha1', '9.7.1'], + ['9.8.2', '9.8.1', '9.8.1-beta1', '9.8.0-alpha1', '9.7.1'], ], 'contrib, semver and legacy' => [ 'aaa_automatic_updates_test.9.8.2.xml', diff --git a/tests/src/Kernel/ReadinessValidation/CronUpdateVersionValidatorTest.php b/tests/src/Kernel/ReadinessValidation/CronUpdateVersionValidatorTest.php deleted file mode 100644 index dbb83d43c0..0000000000 --- a/tests/src/Kernel/ReadinessValidation/CronUpdateVersionValidatorTest.php +++ /dev/null @@ -1,77 +0,0 @@ -<?php - -namespace Drupal\Tests\automatic_updates\Kernel\ReadinessValidation; - -use Drupal\automatic_updates\CronUpdater; -use Drupal\package_manager\ValidationResult; -use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase; -use Drupal\Tests\package_manager\Traits\PackageManagerBypassTestTrait; - -/** - * @covers \Drupal\automatic_updates\Validator\CronUpdateVersionValidator - * - * @group automatic_updates - */ -class CronUpdateVersionValidatorTest extends AutomaticUpdatesKernelTestBase { - - use PackageManagerBypassTestTrait; - - /** - * {@inheritdoc} - */ - protected static $modules = ['automatic_updates']; - - /** - * Data provider for ::testValidationSkippedIfCronUpdatesDisabled(). - * - * @return array[] - * Sets of arguments to pass to the test method. - */ - public function providerValidationSkippedIfCronUpdatesDisabled(): array { - $unstable_current_version = [ - ValidationResult::createError([ - 'Drupal cannot be automatically updated during cron from its current version, 9.7.0-alpha1, because Automatic Updates only supports updating from stable versions during cron.', - ]), - ]; - return [ - 'disabled' => [ - CronUpdater::DISABLED, - [], - ], - 'security only' => [ - CronUpdater::SECURITY, - $unstable_current_version, - ], - 'all' => [ - CronUpdater::ALL, - $unstable_current_version, - ], - ]; - } - - /** - * Tests that validation is skipped if cron updates are disabled. - * - * @param string $cron_setting - * The value of the automatic_updates.settings:cron config setting. - * @param \Drupal\package_manager\ValidationResult[] $expected_results - * The expected validation results. - * - * @dataProvider providerValidationSkippedIfCronUpdatesDisabled - */ - public function testValidationSkippedIfCronUpdatesDisabled(string $cron_setting, array $expected_results): void { - // Set the currently installed version of core to a version that cannot be - // automatically updated, and will always trigger a validation error. This - // way, we can be certain that validation only happens if cron updates are - // enabled. - $this->setCoreVersion('9.7.0-alpha1'); - $this->config('automatic_updates.settings') - ->set('cron', $cron_setting) - ->save(); - - $this->assertCheckerResultsFromManager($expected_results, TRUE); - $this->container->get('cron')->run(); - $this->assertUpdateStagedTimes(0); - } - -} diff --git a/tests/src/Kernel/ReadinessValidation/InstalledVersionValidatorTest.php b/tests/src/Kernel/ReadinessValidation/InstalledVersionValidatorTest.php deleted file mode 100644 index 69ace78cff..0000000000 --- a/tests/src/Kernel/ReadinessValidation/InstalledVersionValidatorTest.php +++ /dev/null @@ -1,38 +0,0 @@ -<?php - -namespace Drupal\Tests\automatic_updates\Kernel\ReadinessValidation; - -use Drupal\automatic_updates\CronUpdater; -use Drupal\package_manager\ValidationResult; -use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase; - -/** - * @covers \Drupal\automatic_updates\Validator\InstalledVersionValidator - * - * @group automatic_updates - */ -class InstalledVersionValidatorTest extends AutomaticUpdatesKernelTestBase { - - /** - * {@inheritdoc} - */ - protected static $modules = ['automatic_updates']; - - /** - * Tests that the installed version of Drupal is checked for updateability. - */ - public function testInstalledVersionValidation(): void { - $this->setCoreVersion('9.8.0-dev'); - // Disable cron to avoid messages from other validators. - // @see \Drupal\automatic_updates\Validator\CronUpdateVersionValidator - $this->config('automatic_updates.settings') - ->set('cron', CronUpdater::DISABLED) - ->save(); - - $result = ValidationResult::createError([ - 'Drupal cannot be automatically updated from the installed version, 9.8.0-dev, because automatic updates from a dev version to any other version are not supported.', - ]); - $this->assertCheckerResultsFromManager([$result], TRUE); - } - -} diff --git a/tests/src/Kernel/ReadinessValidation/UpdateReleaseValidatorTest.php b/tests/src/Kernel/ReadinessValidation/UpdateReleaseValidatorTest.php deleted file mode 100644 index 4234d103dc..0000000000 --- a/tests/src/Kernel/ReadinessValidation/UpdateReleaseValidatorTest.php +++ /dev/null @@ -1,40 +0,0 @@ -<?php - -namespace Drupal\Tests\automatic_updates\Kernel\ReadinessValidation; - -use Drupal\automatic_updates\Exception\UpdateException; -use Drupal\package_manager\ValidationResult; -use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase; - -/** - * @covers \Drupal\automatic_updates\Validator\UpdateReleaseValidator - * - * @group automatic_updates - */ -class UpdateReleaseValidatorTest extends AutomaticUpdatesKernelTestBase { - - /** - * {@inheritdoc} - */ - protected static $modules = ['automatic_updates']; - - /** - * Tests that an error is raised when trying to update to an unknown release. - */ - public function testUnknownReleaseRaisesError(): void { - $result = ValidationResult::createError([ - 'Cannot update Drupal core to 9.8.99 because it is not in the list of installable releases.', - ]); - - try { - $this->container->get('automatic_updates.updater')->begin([ - 'drupal' => '9.8.99', - ]); - $this->fail('Expected an exception to be thrown, but it was not.'); - } - catch (UpdateException $e) { - $this->assertValidationResultsEqual([$result], $e->getResults()); - } - } - -} diff --git a/tests/src/Kernel/ReadinessValidation/UpdateVersionValidatorTest.php b/tests/src/Kernel/ReadinessValidation/UpdateVersionValidatorTest.php deleted file mode 100644 index 3193ada45a..0000000000 --- a/tests/src/Kernel/ReadinessValidation/UpdateVersionValidatorTest.php +++ /dev/null @@ -1,332 +0,0 @@ -<?php - -namespace Drupal\Tests\automatic_updates\Kernel\ReadinessValidation; - -use Drupal\automatic_updates\CronUpdater; -use Drupal\package_manager\ValidationResult; -use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase; -use Drupal\Tests\package_manager\Traits\PackageManagerBypassTestTrait; -use Psr\Log\Test\TestLogger; - -/** - * @covers \Drupal\automatic_updates\Validator\UpdateVersionValidator - * - * @group automatic_updates - */ -class UpdateVersionValidatorTest extends AutomaticUpdatesKernelTestBase { - - use PackageManagerBypassTestTrait; - - /** - * {@inheritdoc} - */ - protected static $modules = ['automatic_updates']; - - /** - * The logger for cron updates. - * - * @var \Psr\Log\Test\TestLogger - */ - private $logger; - - /** - * {@inheritdoc} - */ - protected function setUp(): void { - parent::setUp(); - - $this->logger = new TestLogger(); - $this->container->get('logger.factory') - ->get('automatic_updates') - ->addLogger($this->logger); - } - - /** - * Data provider for all possible cron update frequencies. - * - * @return array[] - * Sets of arguments to pass to the test method. - */ - public function providerOnCurrentVersion(): array { - return [ - 'disabled' => [CronUpdater::DISABLED], - 'security' => [CronUpdater::SECURITY], - 'all' => [CronUpdater::ALL], - ]; - } - - /** - * Tests an update version that is same major & minor version as the current. - * - * @param string $cron_setting - * The value of the automatic_updates.settings:cron config setting. - * - * @dataProvider providerOnCurrentVersion - */ - public function testOnCurrentVersion(string $cron_setting): void { - $this->setCoreVersion('9.8.2'); - $this->config('automatic_updates.settings') - ->set('cron', $cron_setting) - ->save(); - - $this->assertCheckerResultsFromManager([], TRUE); - $this->container->get('cron')->run(); - $this->assertUpdateStagedTimes(0); - } - - /** - * Data provider for ::testMinorUpdates(). - * - * @return array[] - * Sets of arguments to pass to the test method. - */ - public function providerMinorUpdates(): array { - return [ - 'cron disabled, minor updates not allowed' => [ - FALSE, - CronUpdater::DISABLED, - ], - 'cron disabled, minor updates allowed' => [ - TRUE, - CronUpdater::DISABLED, - ], - 'security updates during cron, minor updates not allowed' => [ - FALSE, - CronUpdater::SECURITY, - ], - 'security updates during cron, minor updates allowed' => [ - TRUE, - CronUpdater::SECURITY, - ], - 'cron enabled, minor updates not allowed' => [ - FALSE, - CronUpdater::ALL, - ], - 'cron enabled, minor updates allowed' => [ - TRUE, - CronUpdater::ALL, - ], - ]; - } - - /** - * Tests an update version that is a different minor version than the current. - * - * @param bool $allow_minor_updates - * Whether or not updates across minor core versions are allowed in config. - * @param string $cron_setting - * Whether cron updates are enabled, and how often; should be one of the - * constants in \Drupal\automatic_updates\CronUpdater. This determines which - * stage the validator will use; if cron updates are enabled at all, - * it will be an instance of CronUpdater. - * - * @dataProvider providerMinorUpdates - */ - public function testMinorUpdates(bool $allow_minor_updates, string $cron_setting): void { - $this->config('automatic_updates.settings') - ->set('allow_core_minor_updates', $allow_minor_updates) - ->set('cron', $cron_setting) - ->save(); - - // In order to test what happens when only security updates are enabled - // during cron (the default behavior), ensure that the latest available - // release is a security update. - $this->setReleaseMetadata(['drupal' => __DIR__ . '/../../../fixtures/release-history/drupal.9.8.1-security.xml']); - - $this->setCoreVersion('9.7.1'); - $this->assertCheckerResultsFromManager([], TRUE); - - $this->container->get('cron')->run(); - - $this->assertUpdateStagedTimes(0); - $this->assertEmpty($this->logger->records); - } - - /** - * Data provider for ::testCronUpdateTwoPatchReleasesAhead(). - * - * @return array[] - * Sets of arguments to pass to the test method. - */ - public function providerCronUpdateTwoPatchReleasesAhead(): array { - return [ - 'disabled' => [ - CronUpdater::DISABLED, - [], - 0, - ], - 'security only' => [ - CronUpdater::SECURITY, - [ - ValidationResult::createError([ - 'Drupal cannot be automatically updated during cron from its current version, 9.8.0, to the recommended version, 9.8.1, because 9.8.1 is not a security release.', - ]), - ], - 0, - ], - 'all' => [ - CronUpdater::ALL, - [], - 1, - ], - ]; - } - - /** - * Tests a cron update two patch releases ahead of the current version. - * - * @param string $cron_setting - * The value of the automatic_updates.settings:cron config setting. - * @param \Drupal\package_manager\ValidationResult[] $expected_results - * The expected validation results, which should be logged as errors if the - * update is attempted during cron. - * @param int $expected_stage_times - * The expected number of times the update should have been staged. - * - * @dataProvider providerCronUpdateTwoPatchReleasesAhead - */ - public function testCronUpdateTwoPatchReleasesAhead(string $cron_setting, array $expected_results, int $expected_stage_times): void { - $this->setCoreVersion('9.8.0'); - $this->config('automatic_updates.settings') - ->set('cron', $cron_setting) - ->save(); - - $this->assertCheckerResultsFromManager($expected_results, TRUE); - $this->container->get('cron')->run(); - $this->assertUpdateStagedTimes($expected_stage_times); - } - - /** - * Data provider for ::testCronUpdateOnePatchReleaseAhead(). - * - * @return array[] - * Sets of arguments to pass to the test method. - */ - public function providerCronUpdateOnePatchReleaseAhead(): array { - return [ - 'disabled' => [ - CronUpdater::DISABLED, - FALSE, - ], - // The latest release is not a security update, so the update will only - // happen if cron is updates are allowed for any patch release. - 'security' => [ - CronUpdater::SECURITY, - FALSE, - ], - 'all' => [ - CronUpdater::ALL, - TRUE, - ], - ]; - } - - /** - * Tests a cron update one patch release ahead of the current version. - * - * @param string $cron_setting - * The value of the automatic_updates.settings:cron config setting. - * @param bool $will_update - * TRUE if the update will occur, otherwise FALSE. - * - * @dataProvider providerCronUpdateOnePatchReleaseAhead - */ - public function testCronUpdateOnePatchReleaseAhead(string $cron_setting, bool $will_update): void { - $this->config('automatic_updates.settings') - ->set('cron', $cron_setting) - ->save(); - if ($cron_setting === CronUpdater::SECURITY) { - $expected_result = ValidationResult::createError(['Drupal cannot be automatically updated during cron from its current version, 9.8.1, to the recommended version, 9.8.2, because 9.8.2 is not a security release.']); - $this->assertCheckerResultsFromManager([$expected_result], TRUE); - } - else { - $this->assertCheckerResultsFromManager([], TRUE); - } - $this->container->get('cron')->run(); - $this->assertUpdateStagedTimes((int) $will_update); - } - - /** - * Data provider for ::testInvalidCronUpdate(). - * - * @return array[] - * Sets of arguments to pass to the test method. - */ - public function providerInvalidCronUpdate(): array { - $unstable_current_version = ValidationResult::createError([ - 'Drupal cannot be automatically updated during cron from its current version, 9.8.0-alpha1, because Automatic Updates only supports updating from stable versions during cron.', - ]); - - return [ - 'unstable current version, cron disabled' => [ - CronUpdater::DISABLED, - '9.8.0-alpha1', - // If cron updates are disabled, no error should be flagged, because - // the validation will be run with the regular updater, not the cron - // updater. - [], - ], - 'unstable current version, security updates allowed' => [ - CronUpdater::SECURITY, - '9.8.0-alpha1', - [$unstable_current_version], - ], - 'unstable current version, all updates allowed' => [ - CronUpdater::ALL, - '9.8.0-alpha1', - [$unstable_current_version], - ], - // @todo In the 3 following test cases the installed version is not - // in a supported branch. These test expectations should be changed or - // moved to a new test when we add a validator to check if the installed - // version is in a supported branch in https://www.drupal.org/i/3275883. - 'different current major, cron disabled' => [ - CronUpdater::DISABLED, - '8.9.1', - [], - ], - 'different current major, security updates allowed' => [ - CronUpdater::SECURITY, - '8.9.1', - [], - ], - 'different current major, all updates allowed' => [ - CronUpdater::ALL, - '8.9.1', - [], - ], - ]; - } - - /** - * Tests invalid version jumps before and during a cron update. - * - * @param string $cron_setting - * The value of the automatic_updates.settings:cron config setting. - * @param string $current_core_version - * The current core version from which we are updating. - * @param \Drupal\package_manager\ValidationResult[] $expected_results - * The validation results, if any, that should be flagged during readiness - * checks. - * - * @dataProvider providerInvalidCronUpdate - */ - public function testInvalidCronUpdate(string $cron_setting, string $current_core_version, array $expected_results): void { - $this->setCoreVersion($current_core_version); - $this->config('automatic_updates.settings') - ->set('cron', $cron_setting) - ->save(); - - $this->assertCheckerResultsFromManager($expected_results, TRUE); - - // Try running the update during cron, regardless of the validation results, - // and ensure it doesn't happen. In certain situations, this will be because - // of $cron_setting (e.g., if the latest release is a regular patch release - // but only security updates are allowed during cron); in other situations, - // it will be due to validation errors being raised when the staging area is - // created (in which case, we expect the errors to be logged). - $this->container->get('cron')->run(); - $this->assertUpdateStagedTimes(0); - } - -} diff --git a/tests/src/Kernel/ReadinessValidation/VersionPolicyValidatorTest.php b/tests/src/Kernel/ReadinessValidation/VersionPolicyValidatorTest.php new file mode 100644 index 0000000000..d69cd59fcc --- /dev/null +++ b/tests/src/Kernel/ReadinessValidation/VersionPolicyValidatorTest.php @@ -0,0 +1,377 @@ +<?php + +namespace Drupal\Tests\automatic_updates\Kernel\ReadinessValidation; + +use Drupal\automatic_updates\CronUpdater; +use Drupal\package_manager\Exception\StageValidationException; +use Drupal\package_manager\ValidationResult; +use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase; + +/** + * @covers \Drupal\automatic_updates\Validator\VersionPolicyValidator + * + * @group automatic_updates + */ +class VersionPolicyValidatorTest extends AutomaticUpdatesKernelTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = ['automatic_updates']; + + /** + * Data provider for ::testReadinessCheck(). + * + * @return array[] + * Sets of arguments to pass to the test method. + */ + public function providerReadinessCheck(): array { + $metadata_dir = __DIR__ . '/../../../fixtures/release-history'; + + return [ + // Updating from a dev, alpha, beta, or RC release is not allowed during + // cron. The first case is a control to prove that a legitimate + // patch-level update from a stable release never raises a readiness + // error. + 'stable release installed' => [ + '9.8.0', + "$metadata_dir/drupal.9.8.1-security.xml", + [CronUpdater::DISABLED, CronUpdater::SECURITY, CronUpdater::ALL], + [], + ], + // This case proves that updating from a dev snapshot is never allowed, + // regardless of configuration. + 'dev snapshot installed' => [ + '9.8.0-dev', + "$metadata_dir/drupal.9.8.1-security.xml", + [CronUpdater::DISABLED, CronUpdater::SECURITY, CronUpdater::ALL], + [ + $this->createValidationResult('9.8.0-dev', '9.8.1', [ + 'Drupal cannot be automatically updated from the installed version, 9.8.0-dev, because automatic updates from a dev version to any other version are not supported.', + ]), + ], + ], + // The next six cases prove that updating from an alpha, beta, or RC + // release raises a readiness error if unattended updates are enabled. + 'alpha installed, cron disabled' => [ + '9.8.0-alpha1', + "$metadata_dir/drupal.9.8.1-security.xml", + [CronUpdater::DISABLED], + [], + ], + 'alpha installed, cron enabled' => [ + '9.8.0-alpha1', + "$metadata_dir/drupal.9.8.1-security.xml", + [CronUpdater::SECURITY, CronUpdater::ALL], + [ + $this->createValidationResult('9.8.0-alpha1', '9.8.1', [ + 'Drupal cannot be automatically updated during cron from its current version, 9.8.0-alpha1, because Automatic Updates only supports updating from stable versions during cron.', + ]), + ], + ], + 'beta installed, cron disabled' => [ + '9.8.0-beta2', + "$metadata_dir/drupal.9.8.1-security.xml", + [CronUpdater::DISABLED], + [], + ], + 'beta installed, cron enabled' => [ + '9.8.0-beta2', + "$metadata_dir/drupal.9.8.1-security.xml", + [CronUpdater::SECURITY, CronUpdater::ALL], + [ + $this->createValidationResult('9.8.0-beta2', '9.8.1', [ + 'Drupal cannot be automatically updated during cron from its current version, 9.8.0-beta2, because Automatic Updates only supports updating from stable versions during cron.', + ]), + ], + ], + 'rc installed, cron disabled' => [ + '9.8.0-rc3', + "$metadata_dir/drupal.9.8.1-security.xml", + [CronUpdater::DISABLED], + [], + ], + 'rc installed, cron enabled' => [ + '9.8.0-rc3', + "$metadata_dir/drupal.9.8.1-security.xml", + [CronUpdater::SECURITY, CronUpdater::ALL], + [ + $this->createValidationResult('9.8.0-rc3', '9.8.1', [ + 'Drupal cannot be automatically updated during cron from its current version, 9.8.0-rc3, because Automatic Updates only supports updating from stable versions during cron.', + ]), + ], + ], + // These two cases prove that, if only security updates are allowed + // during cron, a readiness error is raised if the next available release + // is not a security release. + 'update to normal release allowed' => [ + '9.8.1', + "$metadata_dir/drupal.9.8.2.xml", + [CronUpdater::DISABLED, CronUpdater::ALL], + [], + ], + 'update to normal release, security only in cron' => [ + '9.8.1', + "$metadata_dir/drupal.9.8.2.xml", + [CronUpdater::SECURITY], + [ + $this->createValidationResult('9.8.1', '9.8.2', [ + 'Drupal cannot be automatically updated during cron from its current version, 9.8.1, to the recommended version, 9.8.2, because 9.8.2 is not a security release.', + ]), + ], + ], + ]; + } + + /** + * Tests target version validation during readiness checks. + * + * @param string $installed_version + * The installed version of Drupal core. + * @param string $release_metadata + * The path of the core release metadata to serve to the update system. + * @param string[] $cron_modes + * The modes for unattended updates. Can contain any of + * \Drupal\automatic_updates\CronUpdater::DISABLED, + * \Drupal\automatic_updates\CronUpdater::SECURITY, and + * \Drupal\automatic_updates\CronUpdater::ALL. + * @param \Drupal\package_manager\ValidationResult[] $expected_results + * The expected validation results. + * + * @dataProvider providerReadinessCheck + */ + public function testReadinessCheck(string $installed_version, string $release_metadata, array $cron_modes, array $expected_results): void { + $this->setCoreVersion($installed_version); + $this->setReleaseMetadata(['drupal' => $release_metadata]); + + foreach ($cron_modes as $cron_mode) { + $this->config('automatic_updates.settings') + ->set('cron', $cron_mode) + ->save(); + + $this->assertCheckerResultsFromManager($expected_results, TRUE); + } + } + + /** + * Data provider for ::testApi(). + * + * @return array[] + * Sets of arguments to pass to the test method. + */ + public function providerApi(): array { + $metadata_dir = __DIR__ . '/../../../fixtures/release-history'; + + return [ + 'valid target, dev snapshot installed' => [ + ['automatic_updates.updater', 'automatic_updates.cron_updater'], + '9.8.0-dev', + "$metadata_dir/drupal.9.8.1-security.xml", + [CronUpdater::SECURITY, CronUpdater::ALL], + ['drupal' => '9.8.1'], + [ + $this->createValidationResult('9.8.0-dev', '9.8.1', [ + 'Drupal cannot be automatically updated from the installed version, 9.8.0-dev, because automatic updates from a dev version to any other version are not supported.', + ]), + ], + ], + // The following cases can only happen by explicitly supplying the updater + // with an invalid target version. + 'downgrade' => [ + ['automatic_updates.updater', 'automatic_updates.cron_updater'], + '9.8.1', + "$metadata_dir/drupal.9.8.2.xml", + [CronUpdater::SECURITY, CronUpdater::ALL], + ['drupal' => '9.8.0'], + [ + $this->createValidationResult('9.8.1', '9.8.0', [ + 'Update version 9.8.0 is lower than 9.8.1, downgrading is not supported.', + ]), + ], + ], + 'major version upgrade' => [ + ['automatic_updates.updater', 'automatic_updates.cron_updater'], + '8.9.1', + "$metadata_dir/drupal.9.8.2.xml", + [CronUpdater::SECURITY, CronUpdater::ALL], + ['drupal' => '9.8.2'], + [ + $this->createValidationResult('8.9.1', '9.8.2', [ + 'Drupal cannot be automatically updated from its current version, 8.9.1, to the recommended version, 9.8.2, because automatic updates from one major version to another are not supported.', + ]), + ], + ], + 'unsupported target version' => [ + ['automatic_updates.updater', 'automatic_updates.cron_updater'], + '9.8.0', + "$metadata_dir/drupal.9.8.2-unsupported_unpublished.xml", + [CronUpdater::SECURITY, CronUpdater::ALL], + ['drupal' => '9.8.1'], + [ + $this->createValidationResult('9.8.0', '9.8.1', [ + 'Cannot update Drupal core to 9.8.1 because it is not in the list of installable releases.', + ]), + ], + ], + // This case proves that an attended update to a normal non-security + // release is allowed regardless of how cron is configured... + 'attended update to normal release' => [ + ['automatic_updates.updater'], + '9.8.1', + "$metadata_dir/drupal.9.8.2.xml", + [CronUpdater::SECURITY, CronUpdater::ALL], + ['drupal' => '9.8.2'], + [], + ], + // ...and these two cases prove that an unattended update to a normal + // non-security release is only allowed if cron is configured to allow + // all updates. + 'unattended update to normal release, security only in cron' => [ + ['automatic_updates.cron_updater'], + '9.8.1', + "$metadata_dir/drupal.9.8.2.xml", + [CronUpdater::SECURITY], + ['drupal' => '9.8.2'], + [ + $this->createValidationResult('9.8.1', '9.8.2', [ + 'Drupal cannot be automatically updated during cron from its current version, 9.8.1, to the recommended version, 9.8.2, because 9.8.2 is not a security release.', + ]), + ], + ], + 'unattended update to normal release, all allowed in cron' => [ + ['automatic_updates.cron_updater'], + '9.8.1', + "$metadata_dir/drupal.9.8.2.xml", + [CronUpdater::ALL], + ['drupal' => '9.8.2'], + [], + ], + // These three cases prove that updating across minor versions of Drupal + // core is only allowed for attended updates when a specific configuration + // flag is set. + 'unattended update to next minor' => [ + ['automatic_updates.cron_updater'], + '9.7.9', + "$metadata_dir/drupal.9.8.2.xml", + [CronUpdater::SECURITY, CronUpdater::ALL], + ['drupal' => '9.8.2'], + [ + $this->createValidationResult('9.7.9', '9.8.2', [ + 'Drupal cannot be automatically updated from its current version, 9.7.9, to the recommended version, 9.8.2, because automatic updates from one minor version to another are not supported during cron.', + ]), + ], + ], + 'attended update to next minor not allowed' => [ + ['automatic_updates.updater'], + '9.7.9', + "$metadata_dir/drupal.9.8.2.xml", + [CronUpdater::SECURITY, CronUpdater::ALL], + ['drupal' => '9.8.2'], + [ + $this->createValidationResult('9.7.9', '9.8.2', [ + 'Drupal cannot be automatically updated from its current version, 9.7.9, to the recommended version, 9.8.2, because automatic updates from one minor version to another are not supported.', + ]), + ], + ], + 'attended update to next minor allowed' => [ + ['automatic_updates.updater'], + '9.7.9', + "$metadata_dir/drupal.9.8.2.xml", + [CronUpdater::SECURITY, CronUpdater::ALL], + ['drupal' => '9.8.2'], + [], + TRUE, + ], + // Unattended updates to unstable versions are not allowed. + 'unattended update to unstable version' => [ + ['automatic_updates.cron_updater'], + '9.8.0', + "$metadata_dir/drupal.9.8.2-older-sec-release.xml", + [CronUpdater::SECURITY, CronUpdater::ALL], + ['drupal' => '9.8.1-beta1'], + [ + $this->createValidationResult('9.8.0', '9.8.1-beta1', [ + 'Drupal cannot be automatically updated during cron to the recommended version, 9.8.1-beta1, because Automatic Updates only supports updating to stable versions during cron.', + ]), + ], + ], + ]; + } + + /** + * Tests validation of explicitly specified target versions. + * + * @param string[] $updaters + * The IDs of the updater services to test. + * @param string $installed_version + * The installed version of Drupal core. + * @param string $release_metadata + * The path of the core release metadata to serve to the update system. + * @param string[] $cron_modes + * The modes for unattended updates. Can contain + * \Drupal\automatic_updates\CronUpdater::SECURITY or + * \Drupal\automatic_updates\CronUpdater::ALL. + * @param string[] $project_versions + * The desired project versions that should be passed to the updater. + * @param \Drupal\package_manager\ValidationResult[] $expected_results + * The expected validation results. + * @param bool $allow_minor_updates + * (optional) Whether to allow attended updates across minor versions. + * Defaults to FALSE. + * + * @dataProvider providerApi + */ + public function testApi(array $updaters, string $installed_version, string $release_metadata, array $cron_modes, array $project_versions, array $expected_results, bool $allow_minor_updates = FALSE): void { + $this->setCoreVersion($installed_version); + $this->setReleaseMetadata(['drupal' => $release_metadata]); + + foreach ($cron_modes as $cron_mode) { + $this->config('automatic_updates.settings') + ->set('cron', $cron_mode) + ->set('allow_core_minor_updates', $allow_minor_updates) + ->save(); + + foreach ($updaters as $updater) { + /** @var \Drupal\automatic_updates\Updater $updater */ + $updater = $this->container->get($updater); + + try { + $updater->begin($project_versions); + // Ensure that we did not, in fact, expect any errors. + $this->assertEmpty($expected_results); + // Reset the updater for the next iteration of the loop. + $updater->destroy(); + } + catch (StageValidationException $e) { + $this->assertValidationResultsEqual($expected_results, $e->getResults()); + } + } + } + } + + /** + * Creates an expected validation result. + * + * Results returned from VersionPolicyValidator are always summarized in the + * same way, so this method ensures that expected validation results are + * summarized accordingly. + * + * @param string $installed_version + * The installed version of Drupal core. + * @param string $target_version + * The target version of Drupal core. + * @param string[] $messages + * The error messages that the result should contain. + * + * @return \Drupal\package_manager\ValidationResult + * A validation error object with the appropriate summary. + */ + private function createValidationResult(string $installed_version, string $target_version, array $messages): ValidationResult { + $summary = t('Updating from Drupal @installed_version to @target_version is not allowed.', [ + '@installed_version' => $installed_version, + '@target_version' => $target_version, + ]); + return ValidationResult::createError($messages, $summary); + } + +} diff --git a/tests/src/Kernel/ReleaseChooserTest.php b/tests/src/Kernel/ReleaseChooserTest.php index 1576f53947..82c7e686da 100644 --- a/tests/src/Kernel/ReleaseChooserTest.php +++ b/tests/src/Kernel/ReleaseChooserTest.php @@ -34,84 +34,84 @@ class ReleaseChooserTest extends AutomaticUpdatesKernelTestBase { public function providerReleases(): array { return [ 'installed 9.8.0, no minor support' => [ - 'chooser' => 'automatic_updates.release_chooser', + 'updater' => 'automatic_updates.updater', 'minor_support' => FALSE, 'installed_version' => '9.8.0', 'current_minor' => '9.8.2', 'next_minor' => NULL, ], 'installed 9.8.0, minor support' => [ - 'chooser' => 'automatic_updates.release_chooser', + 'updater' => 'automatic_updates.updater', 'minor_support' => TRUE, 'installed_version' => '9.8.0', 'current_minor' => '9.8.2', 'next_minor' => NULL, ], 'installed 9.7.0, no minor support' => [ - 'chooser' => 'automatic_updates.release_chooser', + 'updater' => 'automatic_updates.updater', 'minor_support' => FALSE, 'installed_version' => '9.7.0', 'current_minor' => '9.7.1', 'next_minor' => NULL, ], 'installed 9.7.0, minor support' => [ - 'chooser' => 'automatic_updates.release_chooser', + 'updater' => 'automatic_updates.updater', 'minor_support' => TRUE, 'installed_version' => '9.7.0', 'current_minor' => '9.7.1', 'next_minor' => '9.8.2', ], 'installed 9.7.2, no minor support' => [ - 'chooser' => 'automatic_updates.release_chooser', + 'updater' => 'automatic_updates.updater', 'minor_support' => FALSE, 'installed_version' => '9.7.2', 'current_minor' => NULL, 'next_minor' => NULL, ], 'installed 9.7.2, minor support' => [ - 'chooser' => 'automatic_updates.release_chooser', + 'updater' => 'automatic_updates.updater', 'minor_support' => TRUE, 'installed_version' => '9.7.2', 'current_minor' => NULL, 'next_minor' => '9.8.2', ], 'cron, installed 9.8.0, no minor support' => [ - 'chooser' => 'automatic_updates.cron_release_chooser', + 'updater' => 'automatic_updates.cron_updater', 'minor_support' => FALSE, 'installed_version' => '9.8.0', - 'current_minor' => '9.8.1', + 'current_minor' => '9.8.2', 'next_minor' => NULL, ], 'cron, installed 9.8.0, minor support' => [ - 'chooser' => 'automatic_updates.cron_release_chooser', + 'updater' => 'automatic_updates.cron_updater', 'minor_support' => TRUE, 'installed_version' => '9.8.0', - 'current_minor' => '9.8.1', + 'current_minor' => '9.8.2', 'next_minor' => NULL, ], 'cron, installed 9.7.0, no minor support' => [ - 'chooser' => 'automatic_updates.cron_release_chooser', + 'updater' => 'automatic_updates.cron_updater', 'minor_support' => FALSE, 'installed_version' => '9.7.0', 'current_minor' => '9.7.1', 'next_minor' => NULL, ], 'cron, installed 9.7.0, minor support' => [ - 'chooser' => 'automatic_updates.cron_release_chooser', + 'updater' => 'automatic_updates.cron_updater', 'minor_support' => TRUE, 'installed_version' => '9.7.0', 'current_minor' => '9.7.1', 'next_minor' => NULL, ], 'cron, installed 9.7.2, no minor support' => [ - 'chooser' => 'automatic_updates.cron_release_chooser', + 'updater' => 'automatic_updates.cron_updater', 'minor_support' => FALSE, 'installed_version' => '9.7.2', 'current_minor' => NULL, 'next_minor' => NULL, ], 'cron, installed 9.7.2, minor support' => [ - 'chooser' => 'automatic_updates.cron_release_chooser', + 'updater' => 'automatic_updates.cron_updater', 'minor_support' => TRUE, 'installed_version' => '9.7.2', 'current_minor' => NULL, @@ -123,8 +123,8 @@ class ReleaseChooserTest extends AutomaticUpdatesKernelTestBase { /** * Tests fetching the recommended release when an update is available. * - * @param string $chooser_service - * The ID of release chooser service to use. + * @param string $updater_service + * The ID of the updater service to use. * @param bool $minor_support * Whether updates to the next minor will be allowed. * @param string $installed_version @@ -140,13 +140,15 @@ class ReleaseChooserTest extends AutomaticUpdatesKernelTestBase { * @covers ::getLatestInInstalledMinor * @covers ::getLatestInNextMinor */ - public function testReleases(string $chooser_service, bool $minor_support, string $installed_version, ?string $current_minor, ?string $next_minor): void { + public function testReleases(string $updater_service, bool $minor_support, string $installed_version, ?string $current_minor, ?string $next_minor): void { $this->setCoreVersion($installed_version); $this->config('automatic_updates.settings')->set('allow_core_minor_updates', $minor_support)->save(); /** @var \Drupal\automatic_updates\ReleaseChooser $chooser */ - $chooser = $this->container->get($chooser_service); - $this->assertReleaseVersion($current_minor, $chooser->getLatestInInstalledMinor()); - $this->assertReleaseVersion($next_minor, $chooser->getLatestInNextMinor()); + $chooser = $this->container->get('automatic_updates.release_chooser'); + /** @var \Drupal\automatic_updates\Updater $updater */ + $updater = $this->container->get($updater_service); + $this->assertReleaseVersion($current_minor, $chooser->getLatestInInstalledMinor($updater)); + $this->assertReleaseVersion($next_minor, $chooser->getLatestInNextMinor($updater)); } /** diff --git a/tests/src/Traits/VersionPolicyTestTrait.php b/tests/src/Traits/VersionPolicyTestTrait.php new file mode 100644 index 0000000000..c86355adcd --- /dev/null +++ b/tests/src/Traits/VersionPolicyTestTrait.php @@ -0,0 +1,32 @@ +<?php + +namespace Drupal\Tests\automatic_updates\Traits; + +/** + * Common methods for testing version policy rules. + */ +trait VersionPolicyTestTrait { + + /** + * Tests that a policy rule returns a set of errors. + * + * @param object $rule + * The policy rule under test. + * @param string $installed_version + * The installed version of Drupal. + * @param string|null $target_version + * The target version of Drupal, or NULL if it's not known. + * @param string[] $expected_errors + * The expected error messages, if any. + * @param \Drupal\automatic_updates_9_3_shim\ProjectRelease[] $available_releases + * (optional) The available releases of Drupal core, keyed by version. + * Defaults to an empty array. + */ + protected function assertPolicyErrors(object $rule, string $installed_version, ?string $target_version, array $expected_errors, array $available_releases = []): void { + $rule->setStringTranslation($this->getStringTranslationStub()); + + $actual_errors = array_map('strval', $rule->validate($installed_version, $target_version, $available_releases)); + $this->assertSame($expected_errors, $actual_errors); + } + +} diff --git a/tests/src/Unit/VersionPolicy/ForbidDevSnapshotTest.php b/tests/src/Unit/VersionPolicy/ForbidDevSnapshotTest.php new file mode 100644 index 0000000000..4b91f51115 --- /dev/null +++ b/tests/src/Unit/VersionPolicy/ForbidDevSnapshotTest.php @@ -0,0 +1,64 @@ +<?php + +namespace Drupal\Tests\automatic_updates\Unit\VersionPolicy; + +use Drupal\automatic_updates\Validator\VersionPolicy\ForbidDevSnapshot; +use Drupal\Tests\automatic_updates\Traits\VersionPolicyTestTrait; +use Drupal\Tests\UnitTestCase; + +/** + * @covers \Drupal\automatic_updates\Validator\VersionPolicy\ForbidDevSnapshot + * + * @group automatic_updates + */ +class ForbidDevSnapshotTest extends UnitTestCase { + + use VersionPolicyTestTrait; + + /** + * Data provider for ::testForbidDevSnapshot(). + * + * @return array[] + * Sets of arguments to pass to the test method. + */ + public function providerForbidDevSnapshot(): array { + return [ + 'stable version installed' => [ + '9.8.0', + [], + ], + 'alpha version installed' => [ + '9.8.0-alpha3', + [], + ], + 'beta version installed' => [ + '9.8.0-beta7', + [], + ], + 'release candidate installed' => [ + '9.8.0-rc2', + [], + ], + 'dev snapshot installed' => [ + '9.8.0-dev', + ['Drupal cannot be automatically updated from the installed version, 9.8.0-dev, because automatic updates from a dev version to any other version are not supported.'], + ], + ]; + } + + /** + * Tests that trying to update from a dev snapshot raises an error. + * + * @param string $installed_version + * The installed version of Drupal core. + * @param string[] $expected_errors + * The expected error messages, if any. + * + * @dataProvider providerForbidDevSnapshot + */ + public function testForbidDevSnapshot(string $installed_version, array $expected_errors): void { + $rule = new ForbidDevSnapshot(); + $this->assertPolicyErrors($rule, $installed_version, '9.8.1', $expected_errors); + } + +} diff --git a/tests/src/Unit/VersionPolicy/ForbidDowngradeTest.php b/tests/src/Unit/VersionPolicy/ForbidDowngradeTest.php new file mode 100644 index 0000000000..676c8c255e --- /dev/null +++ b/tests/src/Unit/VersionPolicy/ForbidDowngradeTest.php @@ -0,0 +1,66 @@ +<?php + +namespace Drupal\Tests\automatic_updates\Unit\VersionPolicy; + +use Drupal\automatic_updates\Validator\VersionPolicy\ForbidDowngrade; +use Drupal\Tests\automatic_updates\Traits\VersionPolicyTestTrait; +use Drupal\Tests\UnitTestCase; + +/** + * @covers \Drupal\automatic_updates\Validator\VersionPolicy\ForbidDowngrade + * + * @group automatic_updates + */ +class ForbidDowngradeTest extends UnitTestCase { + + use VersionPolicyTestTrait; + + /** + * Data provider for ::testDowngradeForbidden(). + * + * @return array[] + * Sets of arguments to pass to the test method. + */ + public function providerDowngradeForbidden(): array { + return [ + 'unknown target version' => [ + '9.8.0', + NULL, + ['Update version is lower than 9.8.0, downgrading is not supported.'], + ], + 'same versions' => [ + '9.8.0', + '9.8.0', + [], + ], + 'newer target version' => [ + '9.8.0', + '9.8.2', + [], + ], + 'older target version' => [ + '9.8.2', + '9.8.0', + ['Update version 9.8.0 is lower than 9.8.2, downgrading is not supported.'], + ], + ]; + } + + /** + * Tests that downgrading always raises an error. + * + * @param string $installed_version + * The installed version of Drupal core. + * @param string|null $target_version + * The target version of Drupal core, or NULL if not known. + * @param string[] $expected_errors + * The expected error messages, if any. + * + * @dataProvider providerDowngradeForbidden + */ + public function testDowngradeForbidden(string $installed_version, ?string $target_version, array $expected_errors): void { + $rule = new ForbidDowngrade(); + $this->assertPolicyErrors($rule, $installed_version, $target_version, $expected_errors); + } + +} diff --git a/tests/src/Unit/VersionPolicy/ForbidMinorUpdatesTest.php b/tests/src/Unit/VersionPolicy/ForbidMinorUpdatesTest.php new file mode 100644 index 0000000000..28cb73b9be --- /dev/null +++ b/tests/src/Unit/VersionPolicy/ForbidMinorUpdatesTest.php @@ -0,0 +1,91 @@ +<?php + +namespace Drupal\Tests\automatic_updates\Unit\VersionPolicy; + +use Drupal\automatic_updates\Validator\VersionPolicy\ForbidMinorUpdates; +use Drupal\Tests\automatic_updates\Traits\VersionPolicyTestTrait; +use Drupal\Tests\UnitTestCase; + +/** + * @covers \Drupal\automatic_updates\Validator\VersionPolicy\ForbidMinorUpdates + * + * @group automatic_updates + */ +class ForbidMinorUpdatesTest extends UnitTestCase { + + use VersionPolicyTestTrait; + + /** + * Data provider for ::testMinorUpdateForbidden(). + * + * @return array[] + * Sets of arguments to pass to the test method. + */ + public function providerMinorUpdateForbidden(): array { + return [ + 'same versions' => [ + '9.8.0', + '9.8.0', + [], + ], + 'target version newer in same minor' => [ + '9.8.0', + '9.8.2', + [], + ], + 'target version older in same minor' => [ + '9.8.2', + '9.8.0', + [], + ], + 'target version in older minor' => [ + '9.8.0', + '9.7.2', + ['Drupal cannot be automatically updated from its current version, 9.8.0, to the recommended version, 9.7.2, because automatic updates from one minor version to another are not supported during cron.'], + ], + 'target version in newer minor' => [ + '9.8.0', + '9.9.2', + ['Drupal cannot be automatically updated from its current version, 9.8.0, to the recommended version, 9.9.2, because automatic updates from one minor version to another are not supported during cron.'], + ], + 'target version in older major' => [ + '9.8.0', + '8.8.0', + ['Drupal cannot be automatically updated from its current version, 9.8.0, to the recommended version, 8.8.0, because automatic updates from one minor version to another are not supported during cron.'], + ], + 'target version in newer major' => [ + '9.8.0', + '10.8.0', + ['Drupal cannot be automatically updated from its current version, 9.8.0, to the recommended version, 10.8.0, because automatic updates from one minor version to another are not supported during cron.'], + ], + 'target version in older major and minor' => [ + '9.8.0', + '8.9.9', + ['Drupal cannot be automatically updated from its current version, 9.8.0, to the recommended version, 8.9.9, because automatic updates from one minor version to another are not supported during cron.'], + ], + 'target version in newer major and minor' => [ + '9.8.0', + '10.9.2', + ['Drupal cannot be automatically updated from its current version, 9.8.0, to the recommended version, 10.9.2, because automatic updates from one minor version to another are not supported during cron.'], + ], + ]; + } + + /** + * Tests that trying to update across minor versions raises an error. + * + * @param string $installed_version + * The installed version of Drupal core. + * @param string|null $target_version + * The target version of Drupal core, or NULL if not known. + * @param string[] $expected_errors + * The expected error messages, if any. + * + * @dataProvider providerMinorUpdateForbidden + */ + public function testMinorUpdateForbidden(string $installed_version, ?string $target_version, array $expected_errors): void { + $rule = new ForbidMinorUpdates(); + $this->assertPolicyErrors($rule, $installed_version, $target_version, $expected_errors); + } + +} diff --git a/tests/src/Unit/VersionPolicy/MajorVersionMatchTest.php b/tests/src/Unit/VersionPolicy/MajorVersionMatchTest.php new file mode 100644 index 0000000000..f7b10afc45 --- /dev/null +++ b/tests/src/Unit/VersionPolicy/MajorVersionMatchTest.php @@ -0,0 +1,81 @@ +<?php + +namespace Drupal\Tests\automatic_updates\Unit\VersionPolicy; + +use Drupal\automatic_updates\Validator\VersionPolicy\MajorVersionMatch; +use Drupal\Tests\automatic_updates\Traits\VersionPolicyTestTrait; +use Drupal\Tests\UnitTestCase; + +/** + * @covers \Drupal\automatic_updates\Validator\VersionPolicy\MajorVersionMatch + * + * @group automatic_updates + */ +class MajorVersionMatchTest extends UnitTestCase { + + use VersionPolicyTestTrait; + + /** + * Data provider for ::testMajorVersionMatch(). + * + * @return array[] + * Sets of arguments to pass to the test method. + */ + public function providerMajorVersionMatch(): array { + return [ + 'same versions' => [ + '9.8.0', + '9.8.0', + [], + ], + 'target version newer in same minor' => [ + '9.8.0', + '9.8.2', + [], + ], + 'target version in newer minor' => [ + '9.8.0', + '9.9.2', + [], + ], + 'target version older in same minor' => [ + '9.8.2', + '9.8.0', + [], + ], + 'target version in older minor' => [ + '9.8.0', + '9.7.2', + [], + ], + 'target version in newer major' => [ + '9.8.0', + '10.0.0', + ['Drupal cannot be automatically updated from its current version, 9.8.0, to the recommended version, 10.0.0, because automatic updates from one major version to another are not supported.'], + ], + 'target version in older major' => [ + '9.8.0', + '8.9.0', + ['Drupal cannot be automatically updated from its current version, 9.8.0, to the recommended version, 8.9.0, because automatic updates from one major version to another are not supported.'], + ], + ]; + } + + /** + * Tests that trying to update across major versions raises an error. + * + * @param string $installed_version + * The installed version of Drupal core. + * @param string|null $target_version + * The target version of Drupal core, or NULL if not known. + * @param string[] $expected_errors + * The expected error messages, if any. + * + * @dataProvider providerMajorVersionMatch + */ + public function testMajorVersionMatch(string $installed_version, ?string $target_version, array $expected_errors): void { + $rule = new MajorVersionMatch(); + $this->assertPolicyErrors($rule, $installed_version, $target_version, $expected_errors); + } + +} diff --git a/tests/src/Unit/VersionPolicy/MinorUpdatesEnabledTest.php b/tests/src/Unit/VersionPolicy/MinorUpdatesEnabledTest.php new file mode 100644 index 0000000000..be9616a7f1 --- /dev/null +++ b/tests/src/Unit/VersionPolicy/MinorUpdatesEnabledTest.php @@ -0,0 +1,137 @@ +<?php + +namespace Drupal\Tests\automatic_updates\Unit\VersionPolicy; + +use Drupal\automatic_updates\Validator\VersionPolicy\MinorUpdatesEnabled; +use Drupal\Tests\automatic_updates\Traits\VersionPolicyTestTrait; +use Drupal\Tests\UnitTestCase; + +/** + * @covers \Drupal\automatic_updates\Validator\VersionPolicy\MinorUpdatesEnabled + * + * @group automatic_updates + */ +class MinorUpdatesEnabledTest extends UnitTestCase { + + use VersionPolicyTestTrait; + + /** + * Data provider for ::testMinorUpdatesEnabled(). + * + * @return array[] + * Sets of arguments to pass to the test method. + */ + public function providerMinorUpdatesEnabled(): array { + return [ + 'same versions, minor updates forbidden' => [ + FALSE, + '9.8.0', + '9.8.0', + [], + ], + 'same versions, minor updates allowed' => [ + TRUE, + '9.8.0', + '9.8.0', + [], + ], + 'target version newer in same minor, minor updates forbidden' => [ + FALSE, + '9.8.0', + '9.8.2', + [], + ], + 'target version newer in same minor, minor updates allowed' => [ + TRUE, + '9.8.0', + '9.8.2', + [], + ], + 'target version in newer minor, minor updates forbidden' => [ + FALSE, + '9.8.0', + '9.9.2', + ['Drupal cannot be automatically updated from its current version, 9.8.0, to the recommended version, 9.9.2, because automatic updates from one minor version to another are not supported.'], + ], + 'target version in newer minor, minor updates allowed' => [ + TRUE, + '9.8.0', + '9.9.2', + [], + ], + 'target version older in same minor, minor updates forbidden' => [ + FALSE, + '9.8.2', + '9.8.0', + [], + ], + 'target version older in same minor, minor updates allowed' => [ + TRUE, + '9.8.2', + '9.8.0', + [], + ], + 'target version in older minor, minor updates forbidden' => [ + FALSE, + '9.8.0', + '9.7.2', + ['Drupal cannot be automatically updated from its current version, 9.8.0, to the recommended version, 9.7.2, because automatic updates from one minor version to another are not supported.'], + ], + 'target version in older minor, minor updates allowed' => [ + TRUE, + '9.8.0', + '9.7.2', + [], + ], + 'target version in older major, minor updates forbidden' => [ + FALSE, + '9.8.0', + '8.8.0', + ['Drupal cannot be automatically updated from its current version, 9.8.0, to the recommended version, 8.8.0, because automatic updates from one minor version to another are not supported.'], + ], + 'target version in older major, minor updates allowed' => [ + FALSE, + '9.8.0', + '8.8.0', + ['Drupal cannot be automatically updated from its current version, 9.8.0, to the recommended version, 8.8.0, because automatic updates from one minor version to another are not supported.'], + ], + 'target version in newer major, minor updates forbidden' => [ + FALSE, + '9.8.0', + '10.8.0', + ['Drupal cannot be automatically updated from its current version, 9.8.0, to the recommended version, 10.8.0, because automatic updates from one minor version to another are not supported.'], + ], + 'target version in newer major, minor updates allowed' => [ + FALSE, + '9.8.0', + '10.8.0', + ['Drupal cannot be automatically updated from its current version, 9.8.0, to the recommended version, 10.8.0, because automatic updates from one minor version to another are not supported.'], + ], + ]; + } + + /** + * Tests that trying to update across minor versions depends on configuration. + * + * @param bool $allowed + * Whether or not updating across minor versions is allowed. + * @param string $installed_version + * The installed version of Drupal core. + * @param string|null $target_version + * The target version of Drupal core, or NULL if not known. + * @param string[] $expected_errors + * The expected error messages, if any. + * + * @dataProvider providerMinorUpdatesEnabled + */ + public function testMinorUpdatesEnabled(bool $allowed, string $installed_version, ?string $target_version, array $expected_errors): void { + $config_factory = $this->getConfigFactoryStub([ + 'automatic_updates.settings' => [ + 'allow_core_minor_updates' => $allowed, + ], + ]); + $rule = new MinorUpdatesEnabled($config_factory); + $this->assertPolicyErrors($rule, $installed_version, $target_version, $expected_errors); + } + +} diff --git a/tests/src/Unit/VersionPolicy/StableReleaseInstalledTest.php b/tests/src/Unit/VersionPolicy/StableReleaseInstalledTest.php new file mode 100644 index 0000000000..759b51c6bf --- /dev/null +++ b/tests/src/Unit/VersionPolicy/StableReleaseInstalledTest.php @@ -0,0 +1,60 @@ +<?php + +namespace Drupal\Tests\automatic_updates\Unit\VersionPolicy; + +use Drupal\automatic_updates\Validator\VersionPolicy\StableReleaseInstalled; +use Drupal\Tests\automatic_updates\Traits\VersionPolicyTestTrait; +use Drupal\Tests\UnitTestCase; + +/** + * @covers \Drupal\automatic_updates\Validator\VersionPolicy\StableReleaseInstalled + * + * @group automatic_updates + */ +class StableReleaseInstalledTest extends UnitTestCase { + + use VersionPolicyTestTrait; + + /** + * Data provider for ::testStableReleaseInstalled(). + * + * @return array[] + * Sets of arguments to pass to the test method. + */ + public function providerStableReleaseInstalled(): array { + return [ + 'stable version installed' => [ + '9.8.0', + [], + ], + 'alpha version installed' => [ + '9.8.0-alpha3', + ['Drupal cannot be automatically updated during cron from its current version, 9.8.0-alpha3, because Automatic Updates only supports updating from stable versions during cron.'], + ], + 'beta version installed' => [ + '9.8.0-beta7', + ['Drupal cannot be automatically updated during cron from its current version, 9.8.0-beta7, because Automatic Updates only supports updating from stable versions during cron.'], + ], + 'release candidate installed' => [ + '9.8.0-rc2', + ['Drupal cannot be automatically updated during cron from its current version, 9.8.0-rc2, because Automatic Updates only supports updating from stable versions during cron.'], + ], + ]; + } + + /** + * Tests that trying to update from a non-stable release raises an error. + * + * @param string $installed_version + * The installed version of Drupal core. + * @param string[] $expected_errors + * The expected error messages, if any. + * + * @dataProvider providerStableReleaseInstalled + */ + public function testStableReleaseInstalled(string $installed_version, array $expected_errors): void { + $rule = new StableReleaseInstalled(); + $this->assertPolicyErrors($rule, $installed_version, '9.8.1', $expected_errors); + } + +} diff --git a/tests/src/Unit/VersionPolicy/TargetSecurityReleaseTest.php b/tests/src/Unit/VersionPolicy/TargetSecurityReleaseTest.php new file mode 100644 index 0000000000..386a8e4fdd --- /dev/null +++ b/tests/src/Unit/VersionPolicy/TargetSecurityReleaseTest.php @@ -0,0 +1,68 @@ +<?php + +namespace Drupal\Tests\automatic_updates\Unit\VersionPolicy; + +use Drupal\automatic_updates\Validator\VersionPolicy\TargetSecurityRelease; +use Drupal\automatic_updates_9_3_shim\ProjectRelease; +use Drupal\Tests\automatic_updates\Traits\VersionPolicyTestTrait; +use Drupal\Tests\UnitTestCase; + +/** + * @covers \Drupal\automatic_updates\Validator\VersionPolicy\TargetSecurityRelease + * + * @group automatic_updates + */ +class TargetSecurityReleaseTest extends UnitTestCase { + + use VersionPolicyTestTrait; + + /** + * Data provider for ::testTargetSecurityRelease(). + * + * @return array[] + * Sets of arguments to pass to the test method. + */ + public function providerTargetSecurityRelease(): array { + return [ + 'target security release' => [ + [ + '9.8.1' => ProjectRelease::createFromArray([ + 'status' => 'published', + 'release_link' => 'http://example.com/drupal-9-8-1-release', + 'version' => '9.8.1', + 'terms' => [ + 'Release type' => ['Security update'], + ], + ]), + ], + [], + ], + 'target non-security release' => [ + [ + '9.8.1' => ProjectRelease::createFromArray([ + 'status' => 'published', + 'release_link' => 'http://example.com/drupal-9-8-1-release', + 'version' => '9.8.1', + ]), + ], + ['Drupal cannot be automatically updated during cron from its current version, 9.8.0, to the recommended version, 9.8.1, because 9.8.1 is not a security release.'], + ], + ]; + } + + /** + * Tests that the target version must be a security release. + * + * @param \Drupal\automatic_updates_9_3_shim\ProjectRelease[] $available_releases + * The available releases of Drupal core, keyed by version. + * @param string[] $expected_errors + * The expected error messages, if any. + * + * @dataProvider providerTargetSecurityRelease + */ + public function testTargetSecurityRelease(array $available_releases, array $expected_errors): void { + $rule = new TargetSecurityRelease(); + $this->assertPolicyErrors($rule, '9.8.0', '9.8.1', $expected_errors, $available_releases); + } + +} diff --git a/tests/src/Unit/VersionPolicy/TargetVersionInstallableTest.php b/tests/src/Unit/VersionPolicy/TargetVersionInstallableTest.php new file mode 100644 index 0000000000..ea3e16ee19 --- /dev/null +++ b/tests/src/Unit/VersionPolicy/TargetVersionInstallableTest.php @@ -0,0 +1,69 @@ +<?php + +namespace Drupal\Tests\automatic_updates\Unit\VersionPolicy; + +use Drupal\automatic_updates\Validator\VersionPolicy\TargetVersionInstallable; +use Drupal\automatic_updates_9_3_shim\ProjectRelease; +use Drupal\Tests\automatic_updates\Traits\VersionPolicyTestTrait; +use Drupal\Tests\UnitTestCase; + +/** + * @covers \Drupal\automatic_updates\Validator\VersionPolicy\TargetVersionInstallable + * + * @group automatic_updates + */ +class TargetVersionInstallableTest extends UnitTestCase { + + use VersionPolicyTestTrait; + + /** + * Data provider for ::testTargetVersionInstallable(). + * + * @return array[] + * Sets of arguments to pass to the test method. + */ + public function providerTargetVersionInstallable(): array { + return [ + 'no available releases' => [ + [], + ['Cannot update Drupal core to 9.8.2 because it is not in the list of installable releases.'], + ], + 'unknown target' => [ + [ + '9.8.1' => ProjectRelease::createFromArray([ + 'status' => 'published', + 'release_link' => 'http://example.com/drupal-9-8-1-release', + 'version' => '9.8.1', + ]), + ], + ['Cannot update Drupal core to 9.8.2 because it is not in the list of installable releases.'], + ], + 'valid target' => [ + [ + '9.8.2' => ProjectRelease::createFromArray([ + 'status' => 'published', + 'release_link' => 'http://example.com/drupal-9-8-2-release', + 'version' => '9.8.2', + ]), + ], + [], + ], + ]; + } + + /** + * Tests that the target version must be a known, installable release. + * + * @param \Drupal\automatic_updates_9_3_shim\ProjectRelease[] $available_releases + * The available releases of Drupal core, keyed by version. + * @param string[] $expected_errors + * The expected error messages, if any. + * + * @dataProvider providerTargetVersionInstallable + */ + public function testTargetVersionInstallable(array $available_releases, array $expected_errors): void { + $rule = new TargetVersionInstallable(); + $this->assertPolicyErrors($rule, '9.8.1', '9.8.2', $expected_errors, $available_releases); + } + +} diff --git a/tests/src/Unit/VersionPolicy/TargetVersionStableTest.php b/tests/src/Unit/VersionPolicy/TargetVersionStableTest.php new file mode 100644 index 0000000000..81fd98f44a --- /dev/null +++ b/tests/src/Unit/VersionPolicy/TargetVersionStableTest.php @@ -0,0 +1,64 @@ +<?php + +namespace Drupal\Tests\automatic_updates\Unit\VersionPolicy; + +use Drupal\automatic_updates\Validator\VersionPolicy\TargetVersionStable; +use Drupal\Tests\automatic_updates\Traits\VersionPolicyTestTrait; +use Drupal\Tests\UnitTestCase; + +/** + * @covers \Drupal\automatic_updates\Validator\VersionPolicy\TargetVersionStable + * + * @group automatic_updates + */ +class TargetVersionStableTest extends UnitTestCase { + + use VersionPolicyTestTrait; + + /** + * Data provider for ::testTargetVersionStable(). + * + * @return array[] + * Sets of arguments to pass to the test method. + */ + public function providerTargetVersionStable(): array { + return [ + 'stable target version' => [ + '9.9.0', + [], + ], + 'dev target version' => [ + '9.9.0-dev', + ['Drupal cannot be automatically updated during cron to the recommended version, 9.9.0-dev, because Automatic Updates only supports updating to stable versions during cron.'], + ], + 'alpha target version' => [ + '9.9.0-alpha3', + ['Drupal cannot be automatically updated during cron to the recommended version, 9.9.0-alpha3, because Automatic Updates only supports updating to stable versions during cron.'], + ], + 'beta target version' => [ + '9.9.0-beta7', + ['Drupal cannot be automatically updated during cron to the recommended version, 9.9.0-beta7, because Automatic Updates only supports updating to stable versions during cron.'], + ], + 'release candidate target version' => [ + '9.9.0-rc2', + ['Drupal cannot be automatically updated during cron to the recommended version, 9.9.0-rc2, because Automatic Updates only supports updating to stable versions during cron.'], + ], + ]; + } + + /** + * Tests that trying to update to a non-stable version raises an error. + * + * @param string $target_version + * The target version of Drupal core. + * @param string[] $expected_errors + * The expected error messages, if any. + * + * @dataProvider providerTargetVersionStable + */ + public function testTargetVersionStable(string $target_version, array $expected_errors): void { + $rule = new TargetVersionStable(); + $this->assertPolicyErrors($rule, '9.8.0', $target_version, $expected_errors); + } + +} -- GitLab