diff --git a/package_manager/tests/fixtures/release-history/drupal.9.8.1-extra.xml b/package_manager/tests/fixtures/release-history/drupal.9.8.1-extra.xml new file mode 100644 index 0000000000000000000000000000000000000000..0dbdd739ce17566ba99b0b5635ff00bf92f7a51b --- /dev/null +++ b/package_manager/tests/fixtures/release-history/drupal.9.8.1-extra.xml @@ -0,0 +1,157 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +This fixture is used by \Drupal\Tests\automatic_updates\Kernel\StatusCheck\VersionPolicyValidatorTest. + +Contains metadata about the following (fake) releases of Drupal core, all of which are secure, in order: +- 9.8.1-rc3 +- 9.8.1-beta2 +- 9.8.1-alpha1 +- 9.7.1 +- 9.7.0 +--> +<project xmlns:dc="http://purl.org/dc/elements/1.1/"> + <title>Drupal</title> + <short_name>drupal</short_name> + <dc:creator>Drupal</dc:creator> + <supported_branches>9.7.,9.8.</supported_branches> + <project_status>published</project_status> + <link>http://example.com/project/drupal</link> + <terms> + <term> + <name>Projects</name> + <value>Drupal project</value> + </term> + </terms> + <releases> + <release> + <name>Drupal 9.8.1-rc3</name> + <version>9.8.1-rc3</version> + <status>published</status> + <release_link>http://example.com/drupal-9-8-1-rc3-release</release_link> + <download_link>http://example.com/drupal-9-8-1-rc3.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> + </terms> + </release> + <release> + <name>Drupal 9.8.1-beta2</name> + <version>9.8.1-beta2</version> + <status>published</status> + <release_link>http://example.com/drupal-9-8-1-beta2-release</release_link> + <download_link>http://example.com/drupal-9-8-1-beta2.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> + </terms> + </release> + <release> + <name>Drupal 9.8.1-alpha1</name> + <version>9.8.1-alpha1</version> + <status>published</status> + <release_link>http://example.com/drupal-9-8-1-alpha1-release</release_link> + <download_link>http://example.com/drupal-9-8-1-alpha1.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> + </terms> + </release> + <release> + <name>Drupal 9.8.1-beta2</name> + <version>9.8.1-beta2</version> + <status>published</status> + <release_link>http://example.com/drupal-9-8-1-beta2-release</release_link> + <download_link>http://example.com/drupal-9-8-1-beta2.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> + </terms> + </release> + <release> + <name>Drupal 9.8.0</name> + <version>9.8.0</version> + <status>published</status> + <release_link>http://example.com/drupal-9-8-0-release</release_link> + <download_link>http://example.com/drupal-9-8-0.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>Insecure</value> + </term> + </terms> + </release> + <release> + <name>Drupal 9.7.1</name> + <version>9.7.1</version> + <status>published</status> + <release_link>http://example.com/drupal-9-7-1-release</release_link> + <download_link>http://example.com/drupal-9-7-1.tar.gz</download_link> + <date>1250425521</date> + <terms> + <term> + <name>Release type</name> + <value>New features</value> + </term> + <term> + <name>Release type</name> + <value>Bug fixes</value> + </term> + </terms> + </release> + <release> + <name>Drupal 9.7.0</name> + <version>9.7.0</version> + <status>published</status> + <release_link>http://example.com/drupal-9-7-0-release</release_link> + <download_link>http://example.com/drupal-9-7-0.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> + </terms> + </release> + </releases> +</project> diff --git a/src/Validator/VersionPolicy/MinorUpdatesEnabled.php b/src/Validator/VersionPolicy/MinorUpdatesEnabled.php deleted file mode 100644 index 4a375fd17dc2632c7235a8c3e1e0d7d881b28356..0000000000000000000000000000000000000000 --- a/src/Validator/VersionPolicy/MinorUpdatesEnabled.php +++ /dev/null @@ -1,88 +0,0 @@ -<?php - -declare(strict_types = 1); - -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. - */ -final 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 @installed_version to @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/TargetVersionInstallable.php b/src/Validator/VersionPolicy/TargetVersionInstallable.php index e0a75ae4ed1adf7aa7890f520e62ba363471767d..79ee6a656dcda80292725841d9bed75cc637c499 100644 --- a/src/Validator/VersionPolicy/TargetVersionInstallable.php +++ b/src/Validator/VersionPolicy/TargetVersionInstallable.php @@ -4,22 +4,52 @@ declare(strict_types = 1); 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 requiring the target version to be an installable release. + * A policy rule requiring the target version to be installable. * * @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. */ -final class TargetVersionInstallable { +final class TargetVersionInstallable implements ContainerInjectionInterface { use StringTranslationTrait; + use VersionParsingTrait; /** - * Checks that the target version of Drupal is a known installable release. + * Constructs a TargetVersionInstallable object. + * + * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory + * The config factory service. + */ + public function __construct(private ConfigFactoryInterface $configFactory) {} + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory') + ); + } + + /** + * Checks that the target version can be installed. + * + * This means two things must be true: + * - The target minor version of Drupal can be updated to. The update will + * only be allowed if the allow_core_minor_updates flag is TRUE in config. + * - The target version of Drupal is a known installable release. + * + * If the first check fails, there is no need to do the second check because + * the first check implies that the target version isn't installable. * * @param string $installed_version * The installed version of Drupal. @@ -32,6 +62,22 @@ final class TargetVersionInstallable { * The error messages, if any. */ public function validate(string $installed_version, ?string $target_version, array $available_releases): array { + $installed_minor = static::getMajorAndMinorVersion($installed_version); + $target_minor = static::getMajorAndMinorVersion($target_version); + + if ($installed_minor !== $target_minor) { + $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 @installed_version to @target_version because automatic updates from one minor version to another are not supported.', [ + '@installed_version' => $installed_version, + '@target_version' => $target_version, + ]), + ]; + } + } // 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)) { diff --git a/src/Validator/VersionPolicyValidator.php b/src/Validator/VersionPolicyValidator.php index 998da5a876c6c58e59ca715fc746d21f00840061..7bf7e7421afe64bdb09638ccaf3b71d34e78e4e9 100644 --- a/src/Validator/VersionPolicyValidator.php +++ b/src/Validator/VersionPolicyValidator.php @@ -5,6 +5,7 @@ declare(strict_types = 1); namespace Drupal\automatic_updates\Validator; use Drupal\automatic_updates\CronUpdateStage; +use Drupal\Component\Utility\NestedArray; use Drupal\package_manager\ComposerInspector; use Drupal\package_manager\Event\StatusCheckEvent; use Drupal\package_manager\PathLocator; @@ -13,7 +14,6 @@ use Drupal\automatic_updates\UpdateStage; 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\SupportedBranchInstalled; @@ -110,28 +110,56 @@ final class VersionPolicyValidator implements EventSubscriberInterface { } } } - // 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 = $this->getAvailableReleases($stage); - // 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. + // Let all the rules flag whatever messages they need to. + $messages = []; foreach ($rules as $rule) { - $messages = $this->classResolver - ->getInstanceFromDefinition($rule) + $messages[$rule] = $this->classResolver->getInstanceFromDefinition($rule) ->validate($installed_version, $target_version, $available_releases); + } + // Remove any messages that are superseded by other, more specific ones. + $filtered_rule_messages = array_filter($messages, fn ($rule) => !self::isRuleSuperseded($rule, $messages), ARRAY_FILTER_USE_KEY); + // Collapse all the rules' messages into a single array. + return NestedArray::mergeDeepArray($filtered_rule_messages); + } - if ($messages) { - return $messages; + /** + * Check if a given rule's messages are superseded by a more specific rule. + * + * @param string $rule + * The rule to check. + * @param array[] $rule_messages + * The messages that were returned by the various rules, keyed by the name + * of the rule that returned them. + * + * @return bool + * TRUE if the given rule is superseded by another rule, FALSE otherwise. + */ + private static function isRuleSuperseded(string $rule, array $rule_messages): bool { + // Some rules' messages are more specific than other rules' messages. For + // example, if the message "… automatic updates from one major version to + // another are not supported" is returned, then the message "… not in the + // list of installable releases" is not needed because the new major version + // will not be in the list of installable releases. The keys of this array + // are the rules which supersede messages from the values, which are the + // less specific rules. + $more_specific_rule_sets = [ + ForbidDowngrade::class => [TargetVersionInstallable::class, MajorVersionMatch::class], + ForbidDevSnapshot::class => [StableReleaseInstalled::class], + MajorVersionMatch::class => [TargetVersionInstallable::class], + ForbidMinorUpdates::class => [TargetVersionInstallable::class], + ]; + foreach ($more_specific_rule_sets as $more_specific_rule => $less_specific_rules) { + // If the more specific rule flagged any messages, the given rule is + // superseded. + if (!empty($rule_messages[$more_specific_rule]) && in_array($rule, $less_specific_rules, TRUE)) { + return TRUE; } } - return []; + return FALSE; } /** diff --git a/tests/src/Kernel/AutomaticUpdatesKernelTestBase.php b/tests/src/Kernel/AutomaticUpdatesKernelTestBase.php index a7832fd96a5c48dd95ed9edc9fe46f4d50915a2a..9ac2612e13d94f8a511b40b321c2c4e7256bb5bd 100644 --- a/tests/src/Kernel/AutomaticUpdatesKernelTestBase.php +++ b/tests/src/Kernel/AutomaticUpdatesKernelTestBase.php @@ -112,4 +112,11 @@ class TestCronUpdateStage extends CronUpdateStage { $this->handlePostApply($stage_id, $start_version, $target_version); } + /** + * {@inheritdoc} + */ + public function setMetadata(string $key, $data): void { + parent::setMetadata($key, $data); + } + } diff --git a/tests/src/Kernel/StatusCheck/VersionPolicyValidatorTest.php b/tests/src/Kernel/StatusCheck/VersionPolicyValidatorTest.php index 9d8519c868869321f0ab5d93686d61366f6137d3..c9c8c2e0bc454a48514cd4e19ba194970d7a6dfa 100644 --- a/tests/src/Kernel/StatusCheck/VersionPolicyValidatorTest.php +++ b/tests/src/Kernel/StatusCheck/VersionPolicyValidatorTest.php @@ -31,7 +31,67 @@ class VersionPolicyValidatorTest extends AutomaticUpdatesKernelTestBase { * @return mixed[][] * The test cases. */ - public function providerStatusCheck(): array { + public function providerStatusCheckSpecific(): array { + $metadata_dir = __DIR__ . '/../../../../package_manager/tests/fixtures/release-history'; + + return [ + // This case proves that, if a stable release is installed, there is no + // error generated when if the next available release is a normal (i.e., + // non-security) release. If unattended updates are only enabled for + // security releases, the next available release will be ignored, and + // therefore generate no validation errors, because it's not a security + // release. + 'update to normal release' => [ + '9.8.1', + NULL, + "$metadata_dir/drupal.9.8.2.xml", + [CronUpdateStage::DISABLED, CronUpdateStage::SECURITY, CronUpdateStage::ALL], + [], + ], + // These three cases prove that updating from an unsupported minor version + // will raise an error if unattended updates are enabled. Furthermore, if + // an error is raised, the messaging will vary depending on whether + // attended updates across minor versions are allowed. (Note that the + // target version will not be automatically detected because the release + // metadata used in these cases doesn't have any 9.7.x releases.) + 'update from unsupported minor, cron disabled' => [ + '9.7.1', + NULL, + "$metadata_dir/drupal.9.8.1-security.xml", + [CronUpdateStage::DISABLED], + [], + ], + 'update from unsupported minor, cron enabled, minor updates forbidden' => [ + '9.7.1', + NULL, + "$metadata_dir/drupal.9.8.1-security.xml", + [CronUpdateStage::SECURITY, CronUpdateStage::ALL], + [ + t('The currently installed version of Drupal core, 9.7.1, is not in a supported minor version. Your site will not be automatically updated during cron until it is updated to a supported minor version.'), + t('See the <a href="/admin/reports/updates">available updates page</a> for available updates.'), + ], + ], + 'update from unsupported minor, cron enabled, minor updates allowed' => [ + '9.7.1', + NULL, + "$metadata_dir/drupal.9.8.1-security.xml", + [CronUpdateStage::SECURITY, CronUpdateStage::ALL], + [ + t('The currently installed version of Drupal core, 9.7.1, is not in a supported minor version. Your site will not be automatically updated during cron until it is updated to a supported minor version.'), + t('Use the <a href="/admin/modules/update">update form</a> to update to a supported version.'), + ], + TRUE, + ], + ]; + } + + /** + * Data provider for testStatusCheck() and testCronPreCreate(). + * + * @return mixed[][] + * The test cases. + */ + public function providerGeneric(): array { $metadata_dir = __DIR__ . '/../../../../package_manager/tests/fixtures/release-history'; return [ @@ -40,6 +100,7 @@ class VersionPolicyValidatorTest extends AutomaticUpdatesKernelTestBase { // patch-level update from a stable release never raises an error. 'stable release installed' => [ '9.8.0', + '9.8.1', "$metadata_dir/drupal.9.8.1-security.xml", [CronUpdateStage::DISABLED, CronUpdateStage::SECURITY, CronUpdateStage::ALL], [], @@ -48,108 +109,261 @@ class VersionPolicyValidatorTest extends AutomaticUpdatesKernelTestBase { // regardless of configuration. 'dev snapshot installed' => [ '9.8.0-dev', + '9.8.1', "$metadata_dir/drupal.9.8.1-security.xml", [CronUpdateStage::DISABLED, CronUpdateStage::SECURITY, CronUpdateStage::ALL], [ - $this->createVersionPolicyValidationResult('9.8.0-dev', NULL, [ - t('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.'), - ]), + t('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 an error if unattended updates are enabled. 'alpha installed, cron disabled' => [ '9.8.0-alpha1', + '9.8.1', "$metadata_dir/drupal.9.8.1-security.xml", [CronUpdateStage::DISABLED], [], ], 'alpha installed, cron enabled' => [ '9.8.0-alpha1', + '9.8.1', "$metadata_dir/drupal.9.8.1-security.xml", [CronUpdateStage::SECURITY, CronUpdateStage::ALL], [ - $this->createVersionPolicyValidationResult('9.8.0-alpha1', NULL, [ - t('Drupal cannot be automatically updated during cron from its current version, 9.8.0-alpha1, because it is not a stable version.'), - ]), + t('Drupal cannot be automatically updated during cron from its current version, 9.8.0-alpha1, because it is not a stable version.'), ], ], 'beta installed, cron disabled' => [ '9.8.0-beta2', + '9.8.1', "$metadata_dir/drupal.9.8.1-security.xml", [CronUpdateStage::DISABLED], [], ], 'beta installed, cron enabled' => [ '9.8.0-beta2', + '9.8.1', "$metadata_dir/drupal.9.8.1-security.xml", [CronUpdateStage::SECURITY, CronUpdateStage::ALL], [ - $this->createVersionPolicyValidationResult('9.8.0-beta2', NULL, [ - t('Drupal cannot be automatically updated during cron from its current version, 9.8.0-beta2, because it is not a stable version.'), - ]), + t('Drupal cannot be automatically updated during cron from its current version, 9.8.0-beta2, because it is not a stable version.'), ], ], 'rc installed, cron disabled' => [ '9.8.0-rc3', + '9.8.1', "$metadata_dir/drupal.9.8.1-security.xml", [CronUpdateStage::DISABLED], [], ], 'rc installed, cron enabled' => [ '9.8.0-rc3', + '9.8.1', "$metadata_dir/drupal.9.8.1-security.xml", [CronUpdateStage::SECURITY, CronUpdateStage::ALL], [ - $this->createVersionPolicyValidationResult('9.8.0-rc3', NULL, [ - t('Drupal cannot be automatically updated during cron from its current version, 9.8.0-rc3, because it is not a stable version.'), - ]), + t('Drupal cannot be automatically updated during cron from its current version, 9.8.0-rc3, because it is not a stable version.'), ], ], - // This case proves that, if a stable release is installed, there is no + ]; + } + + /** + * Tests target version validation during status checks. + * + * @param string $installed_version + * The installed version of Drupal core. + * @param string|null $target_version + * The target 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\CronUpdateStage::DISABLED, + * \Drupal\automatic_updates\CronUpdateStage::SECURITY, and + * \Drupal\automatic_updates\CronUpdateStage::ALL. + * @param \Drupal\Core\StringTranslation\TranslatableMarkup[] $expected_validation_messages + * The expected validation messages. + * @param bool $allow_minor_updates + * (optional) Whether or not attended updates across minor updates are + * allowed. Defaults to FALSE. + * + * @dataProvider providerGeneric + * @dataProvider providerStatusCheckSpecific + */ + public function testStatusCheck(string $installed_version, ?string $target_version, string $release_metadata, array $cron_modes, array $expected_validation_messages, 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(); + + $expected_results = []; + if ($expected_validation_messages) { + // If we're doing a status check, the stage isn't created, and the + // requested package versions are not recorded during begin(), so the + // error message won't contain the target version. + $expected_results[] = $this->createVersionPolicyValidationResult($installed_version, NULL, $expected_validation_messages); + } + $this->assertCheckerResultsFromManager($expected_results, TRUE); + } + } + + /** + * Data provider for testCronPreCreate(). + * + * @return mixed[][] + * The test cases. + */ + public function providerCronPreCreateSpecific(): array { + $metadata_dir = __DIR__ . '/../../../../package_manager/tests/fixtures/release-history'; + + return [ + // The next three cases prove that update to an alpha, beta, or RC release + // doesn't raise any error if unattended updates are disabled. + 'update to alpha, cron disabled' => [ + '9.8.0', + '9.8.1-alpha1', + "$metadata_dir/drupal.9.8.1-extra.xml", + [CronUpdateStage::DISABLED], + [], + ], + 'update to beta, cron disabled' => [ + '9.8.0', + '9.8.1-beta2', + "$metadata_dir/drupal.9.8.1-extra.xml", + [CronUpdateStage::DISABLED], + [], + ], + 'update to rc, cron disabled' => [ + '9.8.0', + '9.8.1-rc3', + "$metadata_dir/drupal.9.8.1-extra.xml", + [CronUpdateStage::DISABLED], + [], + ], + // This case proves that, if a stable release is installed, there is an // error generated when if the next available release is a normal (i.e., - // non-security) release. If unattended updates are only enabled for - // security releases, the next available release will be ignored, and - // therefore generate no validation errors, because it's not a security - // release. - 'update to normal release' => [ + // non-security) release, if unattended updates are only enabled for + // security releases. + 'update to normal release, cron enabled for security releases' => [ '9.8.1', + '9.8.2', "$metadata_dir/drupal.9.8.2.xml", - [CronUpdateStage::DISABLED, CronUpdateStage::SECURITY, CronUpdateStage::ALL], - [], + [CronUpdateStage::SECURITY], + [ + t('Drupal cannot be automatically updated during cron from 9.8.1 to 9.8.2 because 9.8.2 is not a security release.'), + ], + ], + // The next three cases prove that normal (i.e., non-security) update to + // an alpha, beta, or RC release raises multiple errors if unattended + // updates are enabled only for security releases. + 'normal update to alpha, cron enabled for security releases' => [ + '9.8.0', + '9.8.1-alpha1', + "$metadata_dir/drupal.9.8.1-extra.xml", + [CronUpdateStage::SECURITY], + [ + t('Drupal cannot be automatically updated during cron to the recommended version, 9.8.1-alpha1, because it is not a stable version.'), + t('Drupal cannot be automatically updated during cron from 9.8.0 to 9.8.1-alpha1 because 9.8.1-alpha1 is not a security release.'), + ], + ], + 'normal update to beta, cron enabled for security releases' => [ + '9.8.0', + '9.8.1-beta2', + "$metadata_dir/drupal.9.8.1-extra.xml", + [CronUpdateStage::SECURITY], + [ + t('Drupal cannot be automatically updated during cron to the recommended version, 9.8.1-beta2, because it is not a stable version.'), + t('Drupal cannot be automatically updated during cron from 9.8.0 to 9.8.1-beta2 because 9.8.1-beta2 is not a security release.'), + ], + ], + 'normal update to rc, cron enabled for security releases' => [ + '9.8.0', + '9.8.1-rc3', + "$metadata_dir/drupal.9.8.1-extra.xml", + [CronUpdateStage::SECURITY], + [ + t('Drupal cannot be automatically updated during cron to the recommended version, 9.8.1-rc3, because it is not a stable version.'), + t('Drupal cannot be automatically updated during cron from 9.8.0 to 9.8.1-rc3 because 9.8.1-rc3 is not a security release.'), + ], + ], + // The next three cases prove that normal (i.e., non-security) minor + // updates to an alpha, beta, or RC release raises multiple errors if + // unattended updates are enabled only for security releases. + 'update to alpha of next minor, cron enabled for security releases, minor updates forbidden' => [ + '9.7.0', + '9.8.1-alpha1', + "$metadata_dir/drupal.9.8.1-extra.xml", + [CronUpdateStage::SECURITY], + [ + t('Drupal cannot be automatically updated during cron to the recommended version, 9.8.1-alpha1, because it is not a stable version.'), + t('Drupal cannot be automatically updated from 9.7.0 to 9.8.1-alpha1 because automatic updates from one minor version to another are not supported during cron.'), + t('Drupal cannot be automatically updated during cron from 9.7.0 to 9.8.1-alpha1 because 9.8.1-alpha1 is not a security release.'), + ], + ], + 'update to beta of next minor, cron enabled for security releases, minor updates forbidden' => [ + '9.7.0', + '9.8.1-beta2', + "$metadata_dir/drupal.9.8.1-extra.xml", + [CronUpdateStage::SECURITY], + [ + t('Drupal cannot be automatically updated during cron to the recommended version, 9.8.1-beta2, because it is not a stable version.'), + t('Drupal cannot be automatically updated from 9.7.0 to 9.8.1-beta2 because automatic updates from one minor version to another are not supported during cron.'), + t('Drupal cannot be automatically updated during cron from 9.7.0 to 9.8.1-beta2 because 9.8.1-beta2 is not a security release.'), + ], + ], + 'update to rc of next minor, cron enabled for security releases, minor updates forbidden' => [ + '9.7.0', + '9.8.1-rc3', + "$metadata_dir/drupal.9.8.1-extra.xml", + [CronUpdateStage::SECURITY], + [ + t('Drupal cannot be automatically updated during cron to the recommended version, 9.8.1-rc3, because it is not a stable version.'), + t('Drupal cannot be automatically updated from 9.7.0 to 9.8.1-rc3 because automatic updates from one minor version to another are not supported during cron.'), + t('Drupal cannot be automatically updated during cron from 9.7.0 to 9.8.1-rc3 because 9.8.1-rc3 is not a security release.'), + ], ], // These three cases prove that updating from an unsupported minor version - // will raise an error if unattended updates are enabled. Furthermore, if - // an error is raised, the messaging will vary depending on whether - // attended updates across minor versions are allowed. (Note that the - // target version will not be automatically detected because the release - // metadata used in these cases doesn't have any 9.7.x releases.) - 'update from unsupported minor, cron disabled' => [ + // will raise an error for unattended updates, if unattended updates are + // enabled. Furthermore, if an error is raised, the messaging will vary + // depending on whether attended updates across minor versions are + // allowed. (Note that the target version will not be automatically + // detected because the release metadata used in these cases doesn't have + // any 9.7.x releases.) + 'update from unsupported minor, cron disabled, minor updates forbidden' => [ '9.7.1', + '9.8.1', "$metadata_dir/drupal.9.8.1-security.xml", [CronUpdateStage::DISABLED], - [], + [ + t('Drupal cannot be automatically updated from 9.7.1 to 9.8.1 because automatic updates from one minor version to another are not supported.'), + ], ], 'update from unsupported minor, cron enabled, minor updates forbidden' => [ '9.7.1', + '9.8.1', "$metadata_dir/drupal.9.8.1-security.xml", [CronUpdateStage::SECURITY, CronUpdateStage::ALL], [ - $this->createVersionPolicyValidationResult('9.7.1', NULL, [ - t('The currently installed version of Drupal core, 9.7.1, is not in a supported minor version. Your site will not be automatically updated during cron until it is updated to a supported minor version.'), - t('See the <a href="/admin/reports/updates">available updates page</a> for available updates.'), - ]), + t('The currently installed version of Drupal core, 9.7.1, is not in a supported minor version. Your site will not be automatically updated during cron until it is updated to a supported minor version.'), + t('See the <a href="/admin/reports/updates">available updates page</a> for available updates.'), + t('Drupal cannot be automatically updated from 9.7.1 to 9.8.1 because automatic updates from one minor version to another are not supported during cron.'), ], ], 'update from unsupported minor, cron enabled, minor updates allowed' => [ '9.7.1', + '9.8.1', "$metadata_dir/drupal.9.8.1-security.xml", [CronUpdateStage::SECURITY, CronUpdateStage::ALL], [ - $this->createVersionPolicyValidationResult('9.7.1', NULL, [ - t('The currently installed version of Drupal core, 9.7.1, is not in a supported minor version. Your site will not be automatically updated during cron until it is updated to a supported minor version.'), - t('Use the <a href="/admin/modules/update">update form</a> to update to a supported version.'), - ]), + t('The currently installed version of Drupal core, 9.7.1, is not in a supported minor version. Your site will not be automatically updated during cron until it is updated to a supported minor version.'), + t('Use the <a href="/admin/modules/update">update form</a> to update to a supported version.'), + t('Drupal cannot be automatically updated from 9.7.1 to 9.8.1 because automatic updates from one minor version to another are not supported during cron.'), ], TRUE, ], @@ -157,10 +371,12 @@ class VersionPolicyValidatorTest extends AutomaticUpdatesKernelTestBase { } /** - * Tests target version validation during status checks. + * Tests target version validation during pre-create. * * @param string $installed_version * The installed version of Drupal core. + * @param string $target_version + * The target 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 @@ -168,25 +384,58 @@ class VersionPolicyValidatorTest extends AutomaticUpdatesKernelTestBase { * \Drupal\automatic_updates\CronUpdateStage::DISABLED, * \Drupal\automatic_updates\CronUpdateStage::SECURITY, and * \Drupal\automatic_updates\CronUpdateStage::ALL. - * @param \Drupal\package_manager\ValidationResult[] $expected_results - * The expected validation results. + * @param \Drupal\Core\StringTranslation\TranslatableMarkup[] $expected_validation_messages + * The expected validation messages. * @param bool $allow_minor_updates * (optional) Whether or not attended updates across minor updates are * allowed. Defaults to FALSE. * - * @dataProvider providerStatusCheck + * @dataProvider providerGeneric + * @dataProvider providerCronPreCreateSpecific */ - public function testStatusCheck(string $installed_version, string $release_metadata, array $cron_modes, array $expected_results, bool $allow_minor_updates = FALSE): void { + public function testCronPreCreate(string $installed_version, string $target_version, string $release_metadata, array $cron_modes, array $expected_validation_messages, bool $allow_minor_updates = FALSE): void { $this->setCoreVersion($installed_version); $this->setReleaseMetadata(['drupal' => $release_metadata]); + // On pre-create, make the stage think that we're updating + // drupal/core-recommended to $target_version. We need to do this to test + // version validation during pre-create of an unattended update. We can't + // use StageFixtureManipulator::setCorePackageVersion() for this, because + // that would get executed after pre-create. + // @see \Drupal\automatic_updates\Validator\VersionPolicyValidator::validateVersion() + $this->addEventTestListener(function (PreCreateEvent $event) use ($target_version): void { + /** @var \Drupal\Tests\automatic_updates\Kernel\TestCronUpdateStage $stage */ + $stage = $event->stage; + $stage->setMetadata('packages', [ + 'production' => [ + 'drupal/core-recommended' => $target_version, + ], + ]); + }, PreCreateEvent::class); + + $expected_results = []; + if ($expected_validation_messages) { + $expected_results[] = $this->createVersionPolicyValidationResult($installed_version, $target_version, $expected_validation_messages); + } + foreach ($cron_modes as $cron_mode) { $this->config('automatic_updates.settings') ->set('cron', $cron_mode) ->set('allow_core_minor_updates', $allow_minor_updates) ->save(); - $this->assertCheckerResultsFromManager($expected_results, TRUE); + $stage = $this->container->get(CronUpdateStage::class); + try { + $stage->create(); + // If we did not get an exception, ensure we didn't expect any results. + $this->assertEmpty($expected_results); + } + catch (StageEventException $e) { + $this->assertExpectedResultsFromException($expected_results, $e); + } + finally { + $stage->destroy(TRUE); + } } } @@ -210,6 +459,72 @@ class VersionPolicyValidatorTest extends AutomaticUpdatesKernelTestBase { ]), ], ], + 'unsupported target, minor version upgrade' => [ + '9.7.1', + "$metadata_dir/drupal.9.8.2-unsupported_unpublished.xml", + ['drupal' => '9.8.1'], + [ + $this->createVersionPolicyValidationResult('9.7.1', '9.8.1', [ + t('Drupal cannot be automatically updated from 9.7.1 to 9.8.1 because automatic updates from one minor version to another are not supported.'), + ]), + ], + ], + 'unsupported target, major version upgrade' => [ + '8.9.1', + "$metadata_dir/drupal.9.8.2-unsupported_unpublished.xml", + ['drupal' => '9.8.1'], + [ + $this->createVersionPolicyValidationResult('8.9.1', '9.8.1', [ + t('Drupal cannot be automatically updated from 8.9.1 to 9.8.1 because automatic updates from one major version to another are not supported.'), + ]), + ], + ], + // The following cases are used to test every combination if a dev + // snapshot is installed. + 'insecure target, dev snapshot installed' => [ + '9.8.0-dev', + "$metadata_dir/drupal.9.8.1-security.xml", + ['drupal' => '9.8.0'], + [ + $this->createVersionPolicyValidationResult('9.8.0-dev', '9.8.0', [ + t('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.'), + t('Cannot update Drupal core to 9.8.0 because it is not in the list of installable releases.'), + ]), + ], + ], + 'downgrade major, dev snapshot installed' => [ + '9.8.0-dev', + "$metadata_dir/drupal.9.8.1-security.xml", + ['drupal' => '8.7.1'], + [ + $this->createVersionPolicyValidationResult('9.8.0-dev', '8.7.1', [ + t('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.'), + t('Update version 8.7.1 is lower than 9.8.0-dev, downgrading is not supported.'), + ]), + ], + ], + 'downgrade minor, dev snapshot installed' => [ + '9.8.0-dev', + "$metadata_dir/drupal.9.8.1-security.xml", + ['drupal' => '9.7.0'], + [ + $this->createVersionPolicyValidationResult('9.8.0-dev', '9.7.0', [ + t('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.'), + t('Update version 9.7.0 is lower than 9.8.0-dev, downgrading is not supported.'), + ]), + ], + ], + 'patch downgrade, dev snapshot installed' => [ + '9.8.1-dev', + "$metadata_dir/drupal.9.8.1-security.xml", + ['drupal' => '9.8.0'], + [ + $this->createVersionPolicyValidationResult('9.8.1-dev', '9.8.0', [ + t('Drupal cannot be automatically updated from the installed version, 9.8.1-dev, because automatic updates from a dev version to any other version are not supported.'), + t('Update version 9.8.0 is lower than 9.8.1-dev, downgrading is not supported.'), + ]), + ], + ], // The following cases can only happen by explicitly supplying the // update stage with an invalid target version. 'downgrade' => [ @@ -332,7 +647,7 @@ class VersionPolicyValidatorTest extends AutomaticUpdatesKernelTestBase { * The installed version of Drupal core. * @param string|null $target_version * The target version of Drupal core, or NULL if it's not known. - * @param string[] $messages + * @param \Drupal\Core\StringTranslation\TranslatableMarkup[] $messages * The error messages that the result should contain. * * @return \Drupal\package_manager\ValidationResult diff --git a/tests/src/Unit/VersionPolicy/MinorUpdatesEnabledTest.php b/tests/src/Unit/VersionPolicy/MinorUpdatesEnabledTest.php deleted file mode 100644 index 9059ef64a93365e5033bfd8926e42bb2545e9695..0000000000000000000000000000000000000000 --- a/tests/src/Unit/VersionPolicy/MinorUpdatesEnabledTest.php +++ /dev/null @@ -1,139 +0,0 @@ -<?php - -declare(strict_types = 1); - -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 - * @internal - */ -class MinorUpdatesEnabledTest extends UnitTestCase { - - use VersionPolicyTestTrait; - - /** - * Data provider for testMinorUpdatesEnabled(). - * - * @return mixed[][] - * The test cases. - */ - 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 9.8.0 to 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 9.8.0 to 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 9.8.0 to 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 9.8.0 to 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 9.8.0 to 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 9.8.0 to 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/TargetVersionInstallableTest.php b/tests/src/Unit/VersionPolicy/TargetVersionInstallableTest.php index 42fd26d56856759232cb0242d20c65034bb522dd..435ba5390320afda772b546f0ae039d1a39ca5ea 100644 --- a/tests/src/Unit/VersionPolicy/TargetVersionInstallableTest.php +++ b/tests/src/Unit/VersionPolicy/TargetVersionInstallableTest.php @@ -27,10 +27,16 @@ class TargetVersionInstallableTest extends UnitTestCase { public function providerTargetVersionInstallable(): array { return [ 'no available releases' => [ + [TRUE, FALSE], + '9.8.1', + '9.8.2', [], ['Cannot update Drupal core to 9.8.2 because it is not in the list of installable releases.'], ], 'unknown target' => [ + [TRUE, FALSE], + '9.8.1', + '9.8.2', [ '9.8.1' => ProjectRelease::createFromArray([ 'status' => 'published', @@ -41,6 +47,9 @@ class TargetVersionInstallableTest extends UnitTestCase { ['Cannot update Drupal core to 9.8.2 because it is not in the list of installable releases.'], ], 'valid target' => [ + [TRUE, FALSE], + '9.8.1', + '9.8.2', [ '9.8.2' => ProjectRelease::createFromArray([ 'status' => 'published', @@ -50,12 +59,126 @@ class TargetVersionInstallableTest extends UnitTestCase { ], [], ], + 'installed version and target version are the same' => [ + [TRUE, FALSE], + '9.8.0', + '9.8.0', + [], + ['Cannot update Drupal core to 9.8.0 because it is not in the list of installable releases.'], + ], + 'unknown patch update' => [ + [TRUE, FALSE], + '9.8.0', + '9.8.2', + [], + ['Cannot update Drupal core to 9.8.2 because it is not in the list of installable releases.'], + ], + 'valid target version newer in same minor' => [ + [TRUE, FALSE], + '9.8.0', + '9.8.2', + [ + '9.8.2' => ProjectRelease::createFromArray([ + 'status' => 'published', + 'release_link' => 'http://example.com/drupal-9-8-2-release', + 'version' => '9.8.2', + ]), + ], + [], + ], + 'target version in newer minor, minor updates forbidden' => [ + [FALSE], + '9.8.0', + '9.9.2', + [], + ['Drupal cannot be automatically updated from 9.8.0 to 9.9.2 because automatic updates from one minor version to another are not supported.'], + ], + 'unknown target version in newer minor, minor updates allowed' => [ + [TRUE], + '9.8.0', + '9.9.2', + [], + ['Cannot update Drupal core to 9.9.2 because it is not in the list of installable releases.'], + ], + 'valid target version in newer minor, minor updates allowed' => [ + [TRUE], + '9.8.0', + '9.9.2', + [ + '9.9.2' => ProjectRelease::createFromArray([ + 'status' => 'published', + 'release_link' => 'http://example.com/drupal-9-9-2-release', + 'version' => '9.9.2', + ]), + ], + [], + ], + 'target version older in same minor' => [ + [TRUE, FALSE], + '9.8.2', + '9.8.0', + [], + ['Cannot update Drupal core to 9.8.0 because it is not in the list of installable releases.'], + ], + 'target version in older minor, minor updates forbidden' => [ + [FALSE], + '9.8.0', + '9.7.2', + [], + ['Drupal cannot be automatically updated from 9.8.0 to 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', + [], + ['Cannot update Drupal core to 9.7.2 because it is not in the list of installable releases.'], + ], + // In practice, the message produced by the next four cases will be + // superseded by the MajorVersionMatch rule. + // @see \Drupal\automatic_updates\Validator\VersionPolicy\MajorVersionMatch + // @see \Drupal\automatic_updates\Validator\VersionPolicyValidator::isRuleSuperseded() + 'target version in older major, minor updates forbidden' => [ + [FALSE], + '9.8.0', + '8.8.0', + [], + ['Drupal cannot be automatically updated from 9.8.0 to 8.8.0 because automatic updates from one minor version to another are not supported.'], + ], + 'target version in older major, minor updates allowed' => [ + [TRUE], + '9.8.0', + '8.8.0', + [], + ['Cannot update Drupal core to 8.8.0 because it is not in the list of installable releases.'], + ], + 'target version in newer major, minor updates forbidden' => [ + [FALSE], + '9.8.0', + '10.8.0', + [], + ['Drupal cannot be automatically updated from 9.8.0 to 10.8.0 because automatic updates from one minor version to another are not supported.'], + ], + 'target version in newer major, minor updates allowed' => [ + [TRUE], + '9.8.0', + '10.8.0', + [], + ['Cannot update Drupal core to 10.8.0 because it is not in the list of installable releases.'], + ], ]; } /** * Tests that the target version must be a known, installable release. * + * @param bool[] $minor_updates_allowed + * The values of the allow_core_minor_updates config flag. The rule will be + * tested separately with each value. + * @param string $installed_version + * The installed version of Drupal core. + * @param string $target_version + * The target version of Drupal core, or NULL if not known. * @param \Drupal\update\ProjectRelease[] $available_releases * The available releases of Drupal core, keyed by version. * @param string[] $expected_errors @@ -63,9 +186,16 @@ class TargetVersionInstallableTest extends UnitTestCase { * * @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); + public function testTargetVersionInstallable(array $minor_updates_allowed, string $installed_version, string $target_version, array $available_releases, array $expected_errors): void { + foreach ($minor_updates_allowed as $value) { + $config_factory = $this->getConfigFactoryStub([ + 'automatic_updates.settings' => [ + 'allow_core_minor_updates' => $value, + ], + ]); + $rule = new TargetVersionInstallable($config_factory); + $this->assertPolicyErrors($rule, $installed_version, $target_version, $expected_errors, $available_releases); + } } }