diff --git a/package_manager/package_manager.module b/package_manager/package_manager.module index d989334c0235b008e2ba4138118937216cd9e79e..55ca66e5d1aa144f392ec7072e7d1a71dbe0571f 100644 --- a/package_manager/package_manager.module +++ b/package_manager/package_manager.module @@ -68,6 +68,23 @@ function package_manager_help($route_name, RouteMatchInterface $route_match) { $output .= ' </li>'; $output .= '</ul>'; + $output .= '<h4 id="package-manager-tuf-info">' . t('Enabling PHP-TUF protection') . '</h4>'; + $output .= '<p>' . t('Package Manager requires <a href=":php-tuf">PHP-TUF</a>, which implements <a href=":tuf">The Update Framework</a> as a way to help secure Composer package downloads via the <a href=":php-tuf-plugin">PHP-TUF Composer integration plugin</a>. This plugin must be installed and configured properly in order to use Package Manager.', [ + ':php-tuf' => 'https://github.com/php-tuf/php-tuf', + ':tuf' => 'https://theupdateframework.io/', + ':php-tuf-plugin' => 'https://github.com/php-tuf/composer-integration', + ]) . '</p>'; + $output .= '<p>' . t('To install and configure the plugin as needed, you can run the following commands:') . '</p>'; + $output .= '<pre><code>'; + $output .= "composer config allow-plugins.php-tuf/composer-integration true\n"; + $output .= "composer require php-tuf/composer-integration"; + $output .= '</code></pre>'; + $output .= '<p>' . t('Package Manager currently requires the <code>https://packages.drupal.org</code> Composer repository to be defined in your <code>composer.json</code> file, since Drupal.org is currently the only package repository that has support for TUF. To set this up, run the following commands (assuming your site is based on the <code>drupal/recommended-project</code> or <code>drupal/legacy-project</code> templates):') . '</p>'; + $output .= '<pre><code>'; + $output .= "composer config --unset repositories.0\n"; + $output .= "composer config repositories.drupal '{\"type\": \"composer\", \"url\": \"https://packages.drupal.org/8\", \"tuf\": true}'\n"; + $output .= '</code></pre>'; + $output .= '<h4 id="package-manager-faq-unsupported-composer-plugin">' . t('What if it says I have unsupported Composer plugins in my codebase?') . '</h4>'; $output .= '<p>' . t('A fresh Drupal installation only uses supported Composer plugins, but some modules or themes may depend on additional Composer plugins. Please <a href=":new-issue">create a new issue</a> when you encounter this.', [ ':new-issue' => 'https://www.drupal.org/node/add/project-issue/automatic_updates', diff --git a/package_manager/package_manager.services.yml b/package_manager/package_manager.services.yml index 735ed8753b8dd35c8b55f61529b9e996ffb1ebb3..786c44ca681d6bf0e48c2aeddf8e484ef074c2c7 100644 --- a/package_manager/package_manager.services.yml +++ b/package_manager/package_manager.services.yml @@ -198,6 +198,9 @@ services: class: Drupal\package_manager\Validator\PhpExtensionsValidator tags: - { name: event_subscriber } + # @todo Tag this service as an event subscriber in https://drupal.org/i/3358504, + # once packages.drupal.org supports TUF. + Drupal\package_manager\Validator\PhpTufValidator: {} package_manager.update_processor: class: Drupal\package_manager\PackageManagerUpdateProcessor arguments: diff --git a/package_manager/src/ComposerInspector.php b/package_manager/src/ComposerInspector.php index 94550aba3e4896a628ecf11b5153f705561ef056..b0b37df64dea9a26df1872ad5d79bd68edd67484 100644 --- a/package_manager/src/ComposerInspector.php +++ b/package_manager/src/ComposerInspector.php @@ -239,6 +239,7 @@ class ComposerInspector implements LoggerAwareInterface { * but if it is a boolean, an array or a map, JSON decoding should be * applied. * + * @see ::getAllowPluginsConfig() * @see \Composer\Command\ConfigCommand::execute() */ public function getConfig(string $key, string $context): ?string { @@ -502,4 +503,32 @@ class ComposerInspector implements LoggerAwareInterface { }; } + /** + * Returns the value of `allow-plugins` config setting. + * + * @param string $dir + * The directory in which to run Composer. + * + * @return bool[]|bool + * An array of boolean flags to allow or disallow certain plugins, or TRUE + * if all plugins are allowed. + * + * @see https://getcomposer.org/doc/06-config.md#allow-plugins + */ + public function getAllowPluginsConfig(string $dir): array|bool { + // If `allow-plugins` is `false`, Composer 2.5.4 and earlier has no output. + $value = $this->getConfig('allow-plugins', $dir) ?? 'false'; + + // Try to convert the value we got back to a boolean. If that can't be done, + // assume it's an array of plugin-specific flags and parse it as JSON. + try { + $value = static::toBoolean($value); + } + catch (\UnhandledMatchError) { + $value = json_decode($value, TRUE, flags: JSON_THROW_ON_ERROR); + } + // An empty array indicates that no plugins are allowed. + return $value ?: []; + } + } diff --git a/package_manager/src/Validator/ComposerPluginsValidator.php b/package_manager/src/Validator/ComposerPluginsValidator.php index 0fdc0268d909a7ea60bcc73d89b85a365526ce75..369f0fb5a6c46b6e2cd8c12394de1f4e64a1fa5b 100644 --- a/package_manager/src/Validator/ComposerPluginsValidator.php +++ b/package_manager/src/Validator/ComposerPluginsValidator.php @@ -85,6 +85,7 @@ final class ComposerPluginsValidator implements EventSubscriberInterface { 'drupal/core-project-message' => '*', 'phpstan/extension-installer' => '^1.1', // cSpell:enable + PhpTufValidator::PLUGIN_NAME => '^1', ]; /** @@ -152,23 +153,13 @@ final class ComposerPluginsValidator implements EventSubscriberInterface { ? $stage->getStageDirectory() : $this->pathLocator->getProjectRoot(); try { - // @see https://getcomposer.org/doc/06-config.md#allow-plugins - $value = $this->inspector->getConfig('allow-plugins', $dir); + $allowed_plugins = $this->inspector->getAllowPluginsConfig($dir); } catch (RuntimeException $exception) { $event->addErrorFromThrowable($exception, $this->t('Unable to determine Composer <code>allow-plugins</code> setting.')); return; } - // Try to convert the value we got back to a boolean. If that can't be done, - // assume it's an array of plugin-specific flags and parse it as JSON. - try { - $allowed_plugins = ComposerInspector::toBoolean($value); - } - catch (\UnhandledMatchError) { - $allowed_plugins = json_decode($value, TRUE, flags: JSON_THROW_ON_ERROR); - } - if ($allowed_plugins === TRUE) { $event->addError([$this->t('All composer plugins are allowed because <code>config.allow-plugins</code> is configured to <code>true</code>. This is an unacceptable security risk.')]); return; diff --git a/package_manager/src/Validator/PhpTufValidator.php b/package_manager/src/Validator/PhpTufValidator.php new file mode 100644 index 0000000000000000000000000000000000000000..a12d8dcc3964d5e722d82d33424a1ff079788f72 --- /dev/null +++ b/package_manager/src/Validator/PhpTufValidator.php @@ -0,0 +1,180 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\package_manager\Validator; + +use Drupal\Component\Serialization\Json; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\Url; +use Drupal\package_manager\ComposerInspector; +use Drupal\package_manager\Event\PreApplyEvent; +use Drupal\package_manager\Event\PreCreateEvent; +use Drupal\package_manager\Event\PreOperationStageEvent; +use Drupal\package_manager\Event\PreRequireEvent; +use Drupal\package_manager\Event\StatusCheckEvent; +use Drupal\package_manager\PathLocator; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * Validates that PHP-TUF is installed and correctly configured. + * + * In both the active and stage directories, this checks for the following + * conditions: + * - The PHP-TUF plugin is installed. + * - The plugin is not explicitly blocked by Composer's `allow-plugins` + * configuration. + * - Composer is aware of at least one repository hosted at + * packages.drupal.org (since that's currently the only server that supports + * TUF), and that those repositories have TUF support explicitly enabled. + * + * Note that this validator is currently not active, because the service + * definition is not tagged as an event subscriber. This will be changed in + * https://drupal.org/i/3358504, once TUF support is rolled out on + * packages.drupal.org. + * + * @internal + * This is an internal part of Package Manager and may be changed or removed + * at any time without warning. External code should not interact with this + * class. + */ +final class PhpTufValidator implements EventSubscriberInterface { + + use StringTranslationTrait; + + /** + * The name of the PHP-TUF Composer integration plugin. + * + * @var string + */ + public const PLUGIN_NAME = 'php-tuf/composer-integration'; + + /** + * Constructs a PhpTufValidator object. + * + * @param \Drupal\package_manager\PathLocator $pathLocator + * The path locator service. + * @param \Drupal\package_manager\ComposerInspector $composerInspector + * The Composer inspector service. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler + * The module handler service. + */ + public function __construct( + private readonly PathLocator $pathLocator, + private readonly ComposerInspector $composerInspector, + private readonly ModuleHandlerInterface $moduleHandler + ) {} + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents(): array { + return [ + StatusCheckEvent::class => 'validate', + PreCreateEvent::class => 'validate', + PreRequireEvent::class => 'validate', + PreApplyEvent::class => 'validate', + ]; + } + + /** + * Reacts to a stage event by validating PHP-TUF configuration as needed. + * + * @param \Drupal\package_manager\Event\PreOperationStageEvent $event + * The event object. + */ + public function validate(PreOperationStageEvent $event): void { + $messages = $this->validateTuf($this->pathLocator->getProjectRoot()); + if ($messages) { + $event->addError($messages, $this->t('The active directory is not protected by PHP-TUF, which is required to use Package Manager securely.')); + } + + $stage = $event->stage; + if ($stage->stageDirectoryExists()) { + $messages = $this->validateTuf($stage->getStageDirectory()); + if ($messages) { + $event->addError($messages, $this->t('The stage directory is not protected by PHP-TUF, which is required to use Package Manager securely.')); + } + } + } + + /** + * Flags messages if PHP-TUF is not installed and configured properly. + * + * @param string $dir + * The directory to examine. + * + * @return \Drupal\Core\StringTranslation\TranslatableMarkup[] + * The error messages, if any. + */ + private function validateTuf(string $dir): array { + $messages = []; + + if ($this->moduleHandler->moduleExists('help')) { + $help_url = Url::fromRoute('help.page', ['name' => 'package_manager']) + ->setOption('fragment', 'package-manager-tuf-info') + ->toString(); + } + + // The Composer plugin must be installed. + $installed_packages = $this->composerInspector->getInstalledPackagesList($dir); + if (!isset($installed_packages[static::PLUGIN_NAME])) { + $message = $this->t('The <code>@plugin</code> plugin is not installed.', [ + '@plugin' => static::PLUGIN_NAME, + ]); + if (isset($help_url)) { + $message = $this->t('@message See <a href=":url">the help page</a> for more information on how to install the plugin.', [ + '@message' => $message, + ':url' => $help_url, + ]); + } + $messages[] = $message; + } + + // And it has to be explicitly enabled. + $allowed_plugins = $this->composerInspector->getAllowPluginsConfig($dir); + if ($allowed_plugins !== TRUE && empty($allowed_plugins[static::PLUGIN_NAME])) { + $message = $this->t('The <code>@plugin</code> plugin is not listed as an allowed plugin.', [ + '@plugin' => static::PLUGIN_NAME, + ]); + if (isset($help_url)) { + $message = $this->t('@message See <a href=":url">the help page</a> for more information on how to configure the plugin.', [ + '@message' => $message, + ':url' => $help_url, + ]); + } + $messages[] = $message; + } + + // Get the defined repositories that use packages.drupal.org. + $repositories = array_filter( + Json::decode($this->composerInspector->getConfig('repositories', $dir)), + fn (array $r): bool => str_starts_with($r['url'], 'https://packages.drupal.org') + ); + + // All packages.drupal.org repositories must have TUF protection. + foreach ($repositories as $repository) { + if (empty($repository['tuf'])) { + $messages[] = $this->t('TUF is not enabled for the @url repository.', [ + '@url' => $repository['url'], + ]); + } + } + + // There must be at least one repository using packages.drupal.org, since + // that's the only repository which supports TUF right now. + if (empty($repositories)) { + $message = $this->t('The <code>https://packages.drupal.org</code> Composer repository must be defined in <code>composer.json</code>.'); + if (isset($help_url)) { + $message = $this->t('@message See <a href=":url">the help page</a> for more information on how to set up this repository.', [ + '@message' => $message, + ':url' => $help_url, + ]); + } + $messages[] = $message; + } + return $messages; + } + +} diff --git a/package_manager/tests/src/Kernel/ComposerInspectorTest.php b/package_manager/tests/src/Kernel/ComposerInspectorTest.php index a645a34b31b6af538c0b258b0db54ca2e568ff0f..5e61f9cdc9472a81b49160682c2237b4034555aa 100644 --- a/package_manager/tests/src/Kernel/ComposerInspectorTest.php +++ b/package_manager/tests/src/Kernel/ComposerInspectorTest.php @@ -410,4 +410,66 @@ class ComposerInspectorTest extends PackageManagerKernelTestBase { $this->assertSame($is_metapackage, is_null($list['test/package']->path)); } + /** + * Data provider for ::testAllowedPlugins(). + * + * @return array[] + * The test cases. + */ + public function providerAllowedPlugins(): array { + return [ + 'all plugins allowed' => [ + ['allow-plugins' => TRUE], + TRUE, + ], + 'no plugins allowed' => [ + ['allow-plugins' => FALSE], + [], + ], + 'some plugins allowed' => [ + [ + 'allow-plugins.example/plugin-a' => TRUE, + 'allow-plugins.example/plugin-b' => FALSE, + ], + [ + 'example/plugin-a' => TRUE, + 'example/plugin-b' => FALSE, + // The scaffold plugin is explicitly disallowed by the fake_site + // fixture. + 'drupal/core-composer-scaffold' => FALSE, + ], + ], + ]; + } + + /** + * Tests ComposerInspector's parsing of the allowed plugins list. + * + * @param array $config + * The Composer configuration to set. + * @param array|bool $expected_value + * The expected return value from getAllowPluginsConfig(). + * + * @covers ::getAllowPluginsConfig + * + * @dataProvider providerAllowedPlugins + */ + public function testAllowedPlugins(array $config, bool|array $expected_value): void { + (new ActiveFixtureManipulator()) + ->addConfig($config) + ->commitChanges(); + + $project_root = $this->container->get(PathLocator::class)->getProjectRoot(); + $actual_value = $this->container->get(ComposerInspector::class) + ->getAllowPluginsConfig($project_root); + + if (is_array($expected_value)) { + ksort($expected_value); + } + if (is_array($actual_value)) { + ksort($actual_value); + } + $this->assertSame($expected_value, $actual_value); + } + } diff --git a/package_manager/tests/src/Kernel/LockFileValidatorTest.php b/package_manager/tests/src/Kernel/LockFileValidatorTest.php index 07127d9e66ddb50d5a2b047632e7860846d736b0..f99e7c9887e009d80b5296e86b9cf0c08375952b 100644 --- a/package_manager/tests/src/Kernel/LockFileValidatorTest.php +++ b/package_manager/tests/src/Kernel/LockFileValidatorTest.php @@ -56,6 +56,7 @@ class LockFileValidatorTest extends PackageManagerKernelTestBase { $inspector->getConfig('extra', $arguments)->willReturn('{}'); $inspector->getConfig('minimum-stability', $arguments)->willReturn('stable'); $inspector->getInstalledPackagesList($arguments)->willReturn(new InstalledPackagesList()); + $inspector->getAllowPluginsConfig($arguments)->willReturn([]); $inspector->validate($arguments); $inspector->getRootPackageInfo($arguments)->willReturn([]); $container->set('package_manager.composer_inspector', $inspector->reveal()); diff --git a/package_manager/tests/src/Kernel/PhpTufValidatorTest.php b/package_manager/tests/src/Kernel/PhpTufValidatorTest.php new file mode 100644 index 0000000000000000000000000000000000000000..cd8c0daa03a6bea4718f499f821d8105907aecb8 --- /dev/null +++ b/package_manager/tests/src/Kernel/PhpTufValidatorTest.php @@ -0,0 +1,250 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\Tests\package_manager\Kernel; + +use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\fixture_manipulator\ActiveFixtureManipulator; +use Drupal\fixture_manipulator\FixtureManipulator; +use Drupal\package_manager\Event\PreApplyEvent; +use Drupal\package_manager\Event\PreCreateEvent; +use Drupal\package_manager\Event\PreRequireEvent; +use Drupal\package_manager\Exception\StageEventException; +use Drupal\package_manager\ValidationResult; +use Drupal\package_manager\Validator\PhpTufValidator; + +/** + * @coversDefaultClass \Drupal\package_manager\Validator\PhpTufValidator + * @group package_manager + * @internal + */ +class PhpTufValidatorTest extends PackageManagerKernelTestBase { + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + (new ActiveFixtureManipulator()) + ->addConfig([ + 'repositories.drupal' => [ + 'type' => 'composer', + 'url' => 'https://packages.drupal.org/8', + 'tuf' => TRUE, + ], + 'allow-plugins.' . PhpTufValidator::PLUGIN_NAME => TRUE, + ]) + ->addPackage([ + 'name' => PhpTufValidator::PLUGIN_NAME, + 'type' => 'composer-plugin', + 'require' => [ + 'composer-plugin-api' => '*', + ], + 'extra' => [ + 'class' => 'PhpTufComposerPlugin', + ], + ]) + ->commitChanges(); + } + + /** + * {@inheritdoc} + */ + public function register(ContainerBuilder $container) { + parent::register($container); + + // @todo Remove this in https://drupal.org/i/3358504, once + // packages.drupal.org supports TUF. + $container->getDefinition(PhpTufValidator::class) + ->addTag('event_subscriber'); + } + + /** + * Tests that there are no errors if the plugin is set up correctly. + */ + public function testPluginInstalledAndConfiguredProperly(): void { + $this->assertStatusCheckResults([]); + $this->assertResults([]); + } + + /** + * Tests there is an error if the plugin is not installed in the project root. + */ + public function testPluginNotInstalledInProjectRoot(): void { + (new ActiveFixtureManipulator()) + ->removePackage(PhpTufValidator::PLUGIN_NAME) + ->commitChanges(); + + $messages = [ + t('The <code>php-tuf/composer-integration</code> plugin is not installed.'), + // Composer automatically removes the plugin from the `allow-plugins` + // list when the plugin package is removed. + t('The <code>php-tuf/composer-integration</code> plugin is not listed as an allowed plugin.'), + ]; + $result = ValidationResult::createError($messages, t('The active directory is not protected by PHP-TUF, which is required to use Package Manager securely.')); + $this->assertStatusCheckResults([$result]); + $this->assertResults([$result], PreCreateEvent::class); + } + + /** + * Tests removing the plugin from the stage on pre-require. + */ + public function testPluginRemovedFromStagePreRequire(): void { + $this->getStageFixtureManipulator() + ->removePackage(PhpTufValidator::PLUGIN_NAME); + + $messages = [ + t('The <code>php-tuf/composer-integration</code> plugin is not installed.'), + // Composer automatically removes the plugin from the `allow-plugins` + // list when the plugin package is removed. + t('The <code>php-tuf/composer-integration</code> plugin is not listed as an allowed plugin.'), + ]; + $result = ValidationResult::createError($messages, t('The stage directory is not protected by PHP-TUF, which is required to use Package Manager securely.')); + $this->assertResults([$result], PreRequireEvent::class); + } + + /** + * Tests removing the plugin from the stage before applying it. + */ + public function testPluginRemovedFromStagePreApply(): void { + $stage = $this->createStage(); + $stage->create(); + $stage->require(['ext-json:*']); + + (new FixtureManipulator()) + ->removePackage(PhpTufValidator::PLUGIN_NAME) + ->commitChanges($stage->getStageDirectory()); + + $messages = [ + t('The <code>php-tuf/composer-integration</code> plugin is not installed.'), + // Composer automatically removes the plugin from the `allow-plugins` + // list when the plugin package is removed. + t('The <code>php-tuf/composer-integration</code> plugin is not listed as an allowed plugin.'), + ]; + $result = ValidationResult::createError($messages, t('The stage directory is not protected by PHP-TUF, which is required to use Package Manager securely.')); + try { + $stage->apply(); + $this->fail('Expected an exception but none was thrown.'); + } + catch (StageEventException $e) { + $this->assertInstanceOf(PreApplyEvent::class, $e->event); + $this->assertValidationResultsEqual([$result], $e->event->getResults()); + } + } + + /** + * Data provider for testing invalid plugin configuration. + * + * @return array[] + * The test cases. + */ + public function providerInvalidConfiguration(): array { + return [ + 'plugin specifically disallowed' => [ + [ + 'allow-plugins.' . PhpTufValidator::PLUGIN_NAME => FALSE, + ], + [ + t('The <code>php-tuf/composer-integration</code> plugin is not listed as an allowed plugin.'), + ], + ], + 'all plugins disallowed' => [ + [ + 'allow-plugins' => FALSE, + ], + [ + t('The <code>php-tuf/composer-integration</code> plugin is not listed as an allowed plugin.'), + ], + ], + 'packages.drupal.org not defined' => [ + [ + 'repositories.drupal' => FALSE, + ], + [ + t('The <code>https://packages.drupal.org</code> Composer repository must be defined in <code>composer.json</code>.'), + ], + ], + 'packages.drupal.org not using TUF' => [ + [ + 'repositories.drupal' => [ + 'type' => 'composer', + 'url' => 'https://packages.drupal.org/8', + ], + ], + [ + t('TUF is not enabled for the https://packages.drupal.org/8 repository.'), + ], + ], + ]; + } + + /** + * Data provider for testing invalid plugin configuration in the stage. + * + * @return \Generator + * The test cases. + */ + public function providerInvalidConfigurationInStage(): \Generator { + foreach ($this->providerInvalidConfiguration() as $name => $arguments) { + $arguments[] = PreRequireEvent::class; + yield "$name on pre-require" => $arguments; + + array_splice($arguments, -1, NULL, PreApplyEvent::class); + yield "$name on pre-apply" => $arguments; + } + } + + /** + * Tests errors caused by invalid plugin configuration in the project root. + * + * @param array $config + * The Composer configuration to set. + * @param \Drupal\Core\StringTranslation\TranslatableMarkup[] $expected_messages + * The expected error messages. + * + * @dataProvider providerInvalidConfiguration + */ + public function testInvalidConfigurationInProjectRoot(array $config, array $expected_messages): void { + (new ActiveFixtureManipulator())->addConfig($config)->commitChanges(); + + $result = ValidationResult::createError($expected_messages, t('The active directory is not protected by PHP-TUF, which is required to use Package Manager securely.')); + $this->assertStatusCheckResults([$result]); + $this->assertResults([$result], PreCreateEvent::class); + } + + /** + * Tests errors caused by invalid plugin configuration in the stage directory. + * + * @param array $config + * The Composer configuration to set. + * @param \Drupal\Core\StringTranslation\TranslatableMarkup[] $expected_messages + * The expected error messages. + * @param string $event_class + * The event before which the plugin's configuration should be changed. + * + * @dataProvider providerInvalidConfigurationInStage + */ + public function testInvalidConfigurationInStage(array $config, array $expected_messages, string $event_class): void { + $listener = function (PreRequireEvent|PreApplyEvent $event) use ($config): void { + (new FixtureManipulator()) + ->addConfig($config) + ->commitChanges($event->stage->getStageDirectory()); + }; + $this->addEventTestListener($listener, $event_class); + + // LockFileValidator will complain because we have not added, removed, or + // updated any packages in the stage. In this very specific situation, it's + // okay to disable that validator to remove the interference. + if ($event_class === PreApplyEvent::class) { + $lock_file_validator = $this->container->get('package_manager.validator.lock_file'); + $this->container->get('event_dispatcher') + ->removeSubscriber($lock_file_validator); + } + + $result = ValidationResult::createError($expected_messages, t('The stage directory is not protected by PHP-TUF, which is required to use Package Manager securely.')); + $this->assertResults([$result], $event_class); + } + +} diff --git a/tests/src/Functional/ClickableHelpTest.php b/tests/src/Functional/ClickableHelpTest.php index 984e760513c8292892e4ca272dedf74a588bcea2..b2db4c23847775fcaf250ad1ebfc29bf1b4ff135 100644 --- a/tests/src/Functional/ClickableHelpTest.php +++ b/tests/src/Functional/ClickableHelpTest.php @@ -4,8 +4,13 @@ declare(strict_types = 1); namespace Drupal\Tests\automatic_updates\Functional; +use Drupal\Core\Url; +use Drupal\package_manager\Event\StatusCheckEvent; +use Drupal\package_manager\ValidationResult; +use Drupal\package_manager_test_validation\EventSubscriber\TestSubscriber; + /** - * Tests package manager help link is clickable. + * Tests that links to online help in validation errors are clickable. * * @group automatic_updates * @internal @@ -18,25 +23,34 @@ class ClickableHelpTest extends AutomaticUpdatesFunctionalTestBase { protected static $modules = [ 'automatic_updates', 'help', + 'package_manager_test_validation', ]; /** * {@inheritdoc} */ - protected $defaultTheme = 'starterkit_theme'; + protected $defaultTheme = 'stark'; /** - * Tests if composer executable is not present then the help link clickable. + * Tests that a link to online help in a validation error is clickable. */ public function testHelpLinkClickable(): void { + $url = Url::fromRoute('help.page', ['name' => 'package_manager']) + ->toString(); + + $result = ValidationResult::createError([ + t('A problem was found! <a href=":url">Read all about it.</a>', [':url' => $url]), + ]); + TestSubscriber::setTestResult([$result], StatusCheckEvent::class); + $this->drupalLogin($this->createUser([ 'administer site configuration', ])); - $this->config('package_manager.settings') - ->set('executables.composer', '/not/matching/path/to/composer') - ->save(); $this->drupalGet('admin/reports/status'); - $this->assertSession()->linkByHrefExists('/admin/help/package_manager#package-manager-composer-related-faq'); + $assert_session = $this->assertSession(); + $assert_session->pageTextContains('A problem was found! Read all about it.'); + $assert_session->linkExists('Read all about it.'); + $assert_session->linkByHrefExists($url); } }