From 21c1c137a5944f0f21ed4ea23cde981ee9898c29 Mon Sep 17 00:00:00 2001 From: Adam G-H <32250-phenaproxima@users.noreply.drupalcode.org> Date: Thu, 6 Jun 2024 01:57:05 +0000 Subject: [PATCH] Issue #3447377 by phenaproxima, chrisfromredfin: Make it possible for Project Browser to activate different kinds of projects --- .../ProjectBrowserSource/RandomDataPlugin.php | 2 +- .../ProjectBrowserSourceExample.php | 4 +- project_browser.services.yml | 9 ++ src/Activator.php | 85 ++++++++++ src/ActivatorInterface.php | 58 +++++++ src/Controller/InstallerController.php | 152 +++++++++--------- src/ModuleActivator.php | 46 ++++++ .../ProjectBrowserSource/DrupalCore.php | 2 +- .../ProjectBrowserSource/MockDrupalDotOrg.php | 2 +- src/ProjectBrowser/Project.php | 26 ++- src/ProjectBrowser/ProjectsResultsPage.php | 11 +- src/Routing/ProjectBrowserRoutes.php | 35 ++-- sveltejs/public/build/bundle.js | Bin 340698 -> 340407 bytes sveltejs/public/build/bundle.js.map | Bin 311876 -> 311357 bytes sveltejs/src/Project/ActionButton.svelte | 2 +- sveltejs/src/Project/AddInstallButton.svelte | 13 +- sveltejs/src/popup.js | 2 +- .../project_browser_test.services.yml | 6 + .../DrupalDotOrgJsonApi.php | 2 +- .../src/TestActivator.php | 44 +++++ .../Functional/InstallerControllerTest.php | 136 ++++++++++------ .../ProjectBrowserInstallerUiTest.php | 12 +- 22 files changed, 471 insertions(+), 178 deletions(-) create mode 100644 src/Activator.php create mode 100644 src/ActivatorInterface.php create mode 100644 src/ModuleActivator.php create mode 100644 tests/modules/project_browser_test/src/TestActivator.php diff --git a/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php b/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php index 720c53872..896e5c677 100644 --- a/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php +++ b/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php @@ -198,7 +198,7 @@ class RandomDataPlugin extends ProjectBrowserSourceBase { author: [ 'name' => $this->randomGenerator->word(10), ], - composerNamespace: 'random/' . $machine_name, + packageName: 'random/' . $machine_name, categories: [$categories[array_rand($categories)]], images: $project_images, ); diff --git a/modules/project_browser_source_example/src/Plugin/ProjectBrowserSource/ProjectBrowserSourceExample.php b/modules/project_browser_source_example/src/Plugin/ProjectBrowserSource/ProjectBrowserSourceExample.php index 6375836f8..5d150425e 100644 --- a/modules/project_browser_source_example/src/Plugin/ProjectBrowserSource/ProjectBrowserSourceExample.php +++ b/modules/project_browser_source_example/src/Plugin/ProjectBrowserSource/ProjectBrowserSourceExample.php @@ -154,7 +154,7 @@ class ProjectBrowserSourceExample extends ProjectBrowserSourceBase { changed: $project_from_source['updated_at'], created: $project_from_source['created_at'], author: $author, - composerNamespace: $project_from_source['composer_namespace'], + packageName: $project_from_source['composer_namespace'], categories: $categories, // Images: Array of images using the same structure as $logo, above. images: [], @@ -182,7 +182,7 @@ class ProjectBrowserSourceExample extends ProjectBrowserSourceBase { changed: $project_from_source['updated_at'], created: $project_from_source['created_at'], author: $author, - composerNamespace: $project_from_source['composer_namespace'], + packageName: $project_from_source['composer_namespace'], categories: $categories, // Images: Array of images using the same structure as $logo, above. images: [], diff --git a/project_browser.services.yml b/project_browser.services.yml index 0d657f753..00cb8599d 100644 --- a/project_browser.services.yml +++ b/project_browser.services.yml @@ -27,3 +27,12 @@ services: arguments: ['@config.factory'] tags: - { name: event_subscriber } + Drupal\project_browser\Activator: + tags: + - { name: service_collector, tag: project_browser.activator, call: addActivator } + Drupal\project_browser\ActivatorInterface: '@Drupal\project_browser\Activator' + Drupal\project_browser\ModuleActivator: + autowire: true + public: false + tags: + - { name: project_browser.activator } diff --git a/src/Activator.php b/src/Activator.php new file mode 100644 index 000000000..9e38e32c3 --- /dev/null +++ b/src/Activator.php @@ -0,0 +1,85 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\project_browser; + +use Drupal\project_browser\ProjectBrowser\Project; +use Symfony\Component\HttpFoundation\Response; + +/** + * A generalized activator that can handle any type of project. + * + * This is a service collector that tries to delegate to the first registered + * activator that says it supports a given project. + */ +final class Activator implements ActivatorInterface { + + /** + * The registered activators. + * + * @var \Drupal\project_browser\ActivatorInterface[] + */ + private array $activators = []; + + /** + * Registers an activator. + * + * @param \Drupal\project_browser\ActivatorInterface $activator + * The activator to register. + */ + public function addActivator(ActivatorInterface $activator): void { + if (in_array($activator, $this->activators, TRUE)) { + return; + } + $this->activators[] = $activator; + } + + /** + * Returns the registered activator to handle a given project. + * + * @param \Drupal\project_browser\ProjectBrowser\Project $project + * A project object. + * + * @return \Drupal\project_browser\ActivatorInterface + * The activator which can handle the given project. + * + * @throws \InvalidArgumentException + * Thrown if none of the registered activators can handle the given project. + */ + private function getActivatorForProject(Project $project): ActivatorInterface { + foreach ($this->activators as $activator) { + if ($activator->supports($project)) { + return $activator; + } + } + throw new \InvalidArgumentException("The project '$project->machineName' is not supported by any registered activators."); + } + + /** + * {@inheritdoc} + */ + public function isActive(Project $project): bool { + return $this->getActivatorForProject($project)->isActive($project); + } + + /** + * {@inheritdoc} + */ + public function supports(Project $project): bool { + try { + return $this->getActivatorForProject($project) instanceof ActivatorInterface; + } + catch (\InvalidArgumentException) { + return FALSE; + } + } + + /** + * {@inheritdoc} + */ + public function activate(Project $project): ?Response { + return $this->getActivatorForProject($project)->activate($project); + } + +} diff --git a/src/ActivatorInterface.php b/src/ActivatorInterface.php new file mode 100644 index 000000000..103707273 --- /dev/null +++ b/src/ActivatorInterface.php @@ -0,0 +1,58 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\project_browser; + +use Drupal\project_browser\ProjectBrowser\Project; +use Symfony\Component\HttpFoundation\Response; + +/** + * Defines an interface for services which can activate projects. + * + * An activator is the "source of truth" about the state of a particular project + * in the current site -- for example, an activator that handles modules knows + * if the module is already installed. + */ +interface ActivatorInterface { + + /** + * Determines if a particular project is activated on the current site. + * + * @param \Drupal\project_browser\ProjectBrowser\Project $project + * A project to check. + * + * @return bool + * TRUE if the project is activated on the current site, FALSE otherwise. + */ + public function isActive(Project $project): bool; + + /** + * Determines if this activator can handle a particular project. + * + * For example, an activator that handles themes might return TRUE from this + * method if the project's Composer package type is `drupal-theme`. + * + * @param \Drupal\project_browser\ProjectBrowser\Project $project + * A project to check. + * + * @return bool + * TRUE if this activator is responsible for the given project, FALSE + * otherwise. + */ + public function supports(Project $project): bool; + + /** + * Activates a project on the current site. + * + * @param \Drupal\project_browser\ProjectBrowser\Project $project + * The project to activate. + * + * @return \Symfony\Component\HttpFoundation\Response|null + * Optionally, a response that should be presented to the user in Project + * Browser. This could be a set of additional instructions to display in a + * modal, for example, or a redirect to a configuration form. + */ + public function activate(Project $project): ?Response; + +} diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php index 126245d5b..87c973c0c 100644 --- a/src/Controller/InstallerController.php +++ b/src/Controller/InstallerController.php @@ -5,11 +5,11 @@ namespace Drupal\project_browser\Controller; use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Access\AccessResult; use Drupal\Core\Controller\ControllerBase; -use Drupal\Core\Extension\ModuleInstallerInterface; use Drupal\Core\TempStore\SharedTempStore; use Drupal\Core\TempStore\SharedTempStoreFactory; use Drupal\Core\Url; use Drupal\package_manager\Exception\StageException; +use Drupal\project_browser\ActivatorInterface; use Drupal\project_browser\ComposerInstaller\Installer; use Drupal\project_browser\EnabledSourceHandler; use Psr\Log\LoggerInterface; @@ -64,22 +64,22 @@ class InstallerController extends ControllerBase { * The installer service. * @param \Drupal\Core\TempStore\SharedTempStoreFactory $shared_temp_store_factory * The temporary storage factory. - * @param \Drupal\Core\Extension\ModuleInstallerInterface $moduleInstaller - * The the module installer. * @param \Drupal\project_browser\EnabledSourceHandler $enabledSourceHandler * The enabled project browser source. * @param \Drupal\Component\Datetime\TimeInterface $time * The system time. * @param \Psr\Log\LoggerInterface $logger * The logger instance. + * @param \Drupal\project_browser\ActivatorInterface $activator + * The project activator service. */ public function __construct( private readonly Installer $installer, SharedTempStoreFactory $shared_temp_store_factory, - private readonly ModuleInstallerInterface $moduleInstaller, private readonly EnabledSourceHandler $enabledSourceHandler, private readonly TimeInterface $time, private readonly LoggerInterface $logger, + private readonly ActivatorInterface $activator, ) { $this->projectBrowserTempStore = $shared_temp_store_factory->get('project_browser'); } @@ -91,10 +91,10 @@ class InstallerController extends ControllerBase { return new static( $container->get('project_browser.installer'), $container->get('tempstore.shared'), - $container->get('module_installer'), $container->get('project_browser.enabled_source'), $container->get('datetime.time'), $container->get('logger.channel.project_browser'), + $container->get(ActivatorInterface::class), ); } @@ -140,8 +140,10 @@ class InstallerController extends ControllerBase { /** * Returns the status of the project in the temp store. * + * @param string $source + * The source plugin ID. * @param string $project_id - * The project machine name. + * The ID of the project, as known to the source plugin. * * @return \Symfony\Component\HttpFoundation\JsonResponse * Information about the project's require/install status. @@ -153,11 +155,14 @@ class InstallerController extends ControllerBase { * regularly so it can monitor the progress of the process and report which * stage is taking place. */ - public function inProgress(string $project_id): JsonResponse { + public function inProgress(string $source, string $project_id): JsonResponse { $requiring = $this->projectBrowserTempStore->get('requiring'); $core_installing = $this->projectBrowserTempStore->get('installing'); $return = ['status' => self::STATUS_IDLE]; + // Prepend the source plugin ID, to create a fully qualified project ID. + $project_id = $source . '/' . $project_id; + if (isset($requiring['project_id']) && $requiring['project_id'] === $project_id) { $return['status'] = self::STATUS_REQUIRING_PROJECT; $return['phase'] = $requiring['phase']; @@ -243,18 +248,22 @@ class InstallerController extends ControllerBase { * Updates the 'requiring' state in the temp store. * * @param string $project_id - * The module being required. + * The fully qualified ID of the project being required. * @param string $phase * The require phase in progress. * @param string $stage_id * The stage id. */ - private function setRequiringState(string $project_id, string $phase, string $stage_id = ''): void { - $this->projectBrowserTempStore->set('requiring', [ - 'project_id' => $project_id, - 'phase' => $phase, - 'stage_id' => $stage_id, - ]); + private function setRequiringState(?string $project_id, string $phase, ?string $stage_id): void { + $data = $this->projectBrowserTempStore->get('requiring') ?? []; + if ($project_id) { + $data['project_id'] = $project_id; + } + if ($stage_id) { + $data['stage_id'] = $stage_id; + } + $data['phase'] = $phase; + $this->projectBrowserTempStore->set('requiring', $data); } /** @@ -322,15 +331,16 @@ class InstallerController extends ControllerBase { /** * Begins requiring by creating a stage. * - * @param string $composer_namespace - * The project composer namespace. + * @param string $source + * The source plugin ID. * @param string $project_id - * The project id. + * The ID of the project, as known to the source plugin. * * @return \Symfony\Component\HttpFoundation\JsonResponse * Status message. */ - public function begin(string $composer_namespace, string $project_id): JsonResponse { + public function begin(string $source, string $project_id): JsonResponse { + $source_id = $source; // @todo Expand to support other plugins in https://drupal.org/i/3312354. $source = $this->enabledSourceHandler->getCurrentSources()['drupalorg_mockapi'] ?? NULL; if ($source === NULL) { @@ -339,6 +349,7 @@ class InstallerController extends ControllerBase { if (!$source->isProjectSafe($project_id)) { return new JsonResponse(['message' => "$project_id is not safe to add because its security coverage has been revoked"], 500); } + $stage_available = $this->installer->isAvailable(); if (!$stage_available) { $requiring_metadata = $this->projectBrowserTempStore->getMetadata('requiring'); @@ -388,7 +399,7 @@ class InstallerController extends ControllerBase { try { $stage_id = $this->installer->create(); - $this->setRequiringState($project_id, 'creating install stage', $stage_id); + $this->setRequiringState($source_id . '/' . $project_id, 'creating install stage', $stage_id); } catch (\Exception $e) { $this->cancelRequire(); @@ -401,49 +412,49 @@ class InstallerController extends ControllerBase { /** * Performs require operations on the stage. * - * @param string $composer_namespace - * The project composer namespace. + * @param string $source + * The source plugin ID. * @param string $project_id - * The project id. - * @param string $stage_id - * ID of stage created in the begin() method. + * The ID of the project, as known to the source plugin. * * @return \Symfony\Component\HttpFoundation\JsonResponse * Status message. */ - public function require(string $composer_namespace, string $project_id, string $stage_id): JsonResponse { + public function require(string $source, string $project_id): JsonResponse { $requiring = $this->projectBrowserTempStore->get('requiring'); - if (empty($requiring['project_id']) || $requiring['project_id'] !== $project_id) { + if (empty($requiring['project_id']) || $requiring['project_id'] !== $source . '/' . $project_id) { return new JsonResponse([ 'message' => sprintf('Error: a request to install %s was ignored as an install for a different module is in progress.', $project_id), ], 500); } - $this->setRequiringState($project_id, 'requiring module', $stage_id); - try { - $this->installer->claim($stage_id)->require(["$composer_namespace/$project_id"]); - } - catch (\Exception $e) { - $this->cancelRequire(); - return $this->errorResponse($e, 'require'); + $this->setRequiringState(NULL, 'requiring module', NULL); + + $projects = $this->enabledSourceHandler->getCurrentSources()[$source]?->getProjects()->list ?? []; + foreach ($projects as $project) { + if ($project->id === $project_id) { + try { + $this->installer->claim($requiring['stage_id'])->require([ + $project->packageName, + ]); + } + catch (\Exception $e) { + $this->cancelRequire(); + return $this->errorResponse($e, 'require'); + } + } } - return $this->successResponse('require', $stage_id); + return $this->successResponse('require', $requiring['stage_id']); } /** * Performs apply operations on the stage. * - * @param string $composer_namespace - * The project composer namespace. - * @param string $project_id - * The project id. - * @param string $stage_id - * ID of stage created in the begin() method. - * * @return \Symfony\Component\HttpFoundation\JsonResponse * Status message. */ - public function apply(string $composer_namespace, string $project_id, string $stage_id): JsonResponse { - $this->setRequiringState($project_id, 'applying', $stage_id); + public function apply(): JsonResponse { + $stage_id = $this->projectBrowserTempStore->get('requiring')['stage_id']; + $this->setRequiringState(NULL, 'applying', NULL); try { $this->installer->claim($stage_id)->apply(); } @@ -457,18 +468,12 @@ class InstallerController extends ControllerBase { /** * Performs post apply operations on the stage. * - * @param string $composer_namespace - * The project composer namespace. - * @param string $project_id - * The project id. - * @param string $stage_id - * ID of stage created in the begin() method. - * * @return \Symfony\Component\HttpFoundation\JsonResponse * Status message. */ - public function postApply(string $composer_namespace, string $project_id, string $stage_id): JsonResponse { - $this->setRequiringState($project_id, 'post apply', $stage_id); + public function postApply(): JsonResponse { + $stage_id = $this->projectBrowserTempStore->get('requiring')['stage_id']; + $this->setRequiringState(NULL, 'post apply', NULL); try { $this->installer->claim($stage_id)->postApply(); } @@ -481,18 +486,12 @@ class InstallerController extends ControllerBase { /** * Performs destroy operations on the stage. * - * @param string $composer_namespace - * The project composer namespace. - * @param string $project_id - * The project id. - * @param string $stage_id - * ID of stage created in the begin() method. - * * @return \Symfony\Component\HttpFoundation\JsonResponse * Status message. */ - public function destroy(string $composer_namespace, string $project_id, string $stage_id): JsonResponse { - $this->setRequiringState($project_id, 'completing', $stage_id); + public function destroy(): JsonResponse { + $stage_id = $this->projectBrowserTempStore->get('requiring')['stage_id']; + $this->setRequiringState(NULL, 'completing', NULL); try { $this->installer->claim($stage_id)->destroy(); } @@ -510,25 +509,32 @@ class InstallerController extends ControllerBase { /** * Installs an already downloaded module. * + * @param string $source + * The source plugin ID. * @param string $project_id - * The project machine name. + * The ID of the project, as known to the source plugin. * * @return \Symfony\Component\HttpFoundation\JsonResponse * Status message. */ - public function activateModule(string $project_id): JsonResponse { + public function activate(string $source, string $project_id): JsonResponse { $this->projectBrowserTempStore->set('installing', $project_id); - try { - $this->moduleInstaller->install([$project_id]); - } - catch (\Exception $e) { - $this->resetProgress(); - return $this->errorResponse($e, 'project install'); + + $projects = $this->enabledSourceHandler->getCurrentSources()[$source]?->getProjects()->list ?? []; + foreach ($projects as $project) { + if ($project->id === $project_id) { + try { + $this->activator->activate($project); + } + catch (\Throwable $e) { + return $this->errorResponse($e, 'project install'); + } + finally { + $this->resetProgress(); + } + } } - $this->projectBrowserTempStore->delete('installing'); - return new JsonResponse([ - 'status' => 0, - ]); + return new JsonResponse(['status' => 0]); } } diff --git a/src/ModuleActivator.php b/src/ModuleActivator.php new file mode 100644 index 000000000..08846840d --- /dev/null +++ b/src/ModuleActivator.php @@ -0,0 +1,46 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\project_browser; + +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Extension\ModuleInstallerInterface; +use Drupal\project_browser\ProjectBrowser\Project; +use Symfony\Component\HttpFoundation\Response; + +/** + * An activator for Drupal modules. + */ +final class ModuleActivator implements ActivatorInterface { + + public function __construct( + private readonly ModuleHandlerInterface $moduleHandler, + private readonly ModuleInstallerInterface $moduleInstaller, + ) {} + + /** + * {@inheritdoc} + */ + public function isActive(Project $project): bool { + return $this->moduleHandler->moduleExists($project->machineName); + } + + /** + * {@inheritdoc} + */ + public function supports(Project $project): bool { + // At the moment, Project Browser only supports modules, so all projects can + // be handled by this activator. + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function activate(Project $project): ?Response { + $this->moduleInstaller->install([$project->machineName]); + return NULL; + } + +} diff --git a/src/Plugin/ProjectBrowserSource/DrupalCore.php b/src/Plugin/ProjectBrowserSource/DrupalCore.php index b58136cdf..80c5fd2cf 100644 --- a/src/Plugin/ProjectBrowserSource/DrupalCore.php +++ b/src/Plugin/ProjectBrowserSource/DrupalCore.php @@ -205,7 +205,7 @@ class DrupalCore extends ProjectBrowserSourceBase { author: [ 'name' => 'Drupal Core', ], - composerNamespace: '', + packageName: '', categories: [ [ 'id' => $module->info['package'], diff --git a/src/Plugin/ProjectBrowserSource/MockDrupalDotOrg.php b/src/Plugin/ProjectBrowserSource/MockDrupalDotOrg.php index 8995d2b6e..c1cb103a3 100644 --- a/src/Plugin/ProjectBrowserSource/MockDrupalDotOrg.php +++ b/src/Plugin/ProjectBrowserSource/MockDrupalDotOrg.php @@ -385,7 +385,7 @@ class MockDrupalDotOrg extends ProjectBrowserSourceBase { changed: $project_data['changed'], created: $project_data['created'], author: ['name' => $project_data['author']], - composerNamespace: 'drupal/' . $project_data['field_project_machine_name'], + packageName: 'drupal/' . $project_data['field_project_machine_name'], url: 'https://www.drupal.org/project/' . $project_data['field_project_machine_name'], // Add name property to each category, so it can be rendered. categories: array_map(fn($category) => $categories[$category['id']] ?? '', $project_data['project_data']['taxonomy_vocabulary_3'] ?? []), diff --git a/src/ProjectBrowser/Project.php b/src/ProjectBrowser/Project.php index 805538e43..32d08f252 100644 --- a/src/ProjectBrowser/Project.php +++ b/src/ProjectBrowser/Project.php @@ -11,6 +11,13 @@ use Drupal\Component\Utility\Xss; */ class Project implements \JsonSerializable { + /** + * The unqualified project ID. + * + * @var string + */ + public readonly string $id; + /** * Constructs a Project object. * @@ -42,8 +49,8 @@ class Project implements \JsonSerializable { * When was the project created last timestamp. * @param array $author * Author of the project in array format. - * @param string $composerNamespace - * Composer namespace of the project. + * @param string $packageName + * The Composer package name of this project, e.g. `drupal/project_browser`. * @param string $url * URL of the project. * @param array $categories @@ -64,6 +71,7 @@ class Project implements \JsonSerializable { * the contents of the "View Commands" popup. * To include a paste-able command that includes a copy button, use this * markup structure: + * * @code * <div class="command-box"> * <input value="THE_COMMAND_TO_BE_COPIED" readonly="" /> @@ -72,6 +80,9 @@ class Project implements \JsonSerializable { * </button> * </div> * @endcode + * @param string $id + * (optional) The unqualified project ID. Cannot contain a slash. Defaults + * to the machine name. */ public function __construct( public array $logo, @@ -88,15 +99,22 @@ class Project implements \JsonSerializable { public int $changed, public int $created, public array $author, - public string $composerNamespace, + public string $packageName, public string $url = '', public array $categories = [], public array $images = [], public array $warnings = [], public string $type = 'module:drupalorg', public string|bool $commands = FALSE, + string $id = '', ) { $this->setSummary($body); + // @see \Drupal\project_browser\ProjectBrowser\ProjectsResultsPage::jsonSerialize() + // @see \Drupal\project_browser\Routing\ProjectBrowserRoutes::routes() + if (str_contains($id, '/')) { + throw new \InvalidArgumentException("Project IDs cannot contain slashes."); + } + $this->id = $id ? $id : $machineName; } /** @@ -144,7 +162,7 @@ class Project implements \JsonSerializable { 'title' => $this->title, 'author' => $this->author, 'warnings' => $this->warnings, - 'composer_namespace' => $this->composerNamespace, + 'package_name' => $this->packageName, // @todo Not used in Svelte. Audit in https://www.drupal.org/i/3309273. 'is_maintained' => $this->isMaintained, 'is_active' => $this->isActive, diff --git a/src/ProjectBrowser/ProjectsResultsPage.php b/src/ProjectBrowser/ProjectsResultsPage.php index 6660df92f..5f520abaa 100644 --- a/src/ProjectBrowser/ProjectsResultsPage.php +++ b/src/ProjectBrowser/ProjectsResultsPage.php @@ -7,15 +7,6 @@ namespace Drupal\project_browser\ProjectBrowser; */ class ProjectsResultsPage implements \JsonSerializable { - /** - * Separates the source plugin ID from a project's local ID. - * - * @var string - * - * @see ::jsonSerialize() - */ - public const ID_SEPARATOR = '::'; - /** * Constructor for project browser results page. * @@ -48,7 +39,7 @@ class ProjectsResultsPage implements \JsonSerializable { $map = function (Project $project): object { $serialized = $project->jsonSerialize(); - $serialized->id = $this->pluginId . static::ID_SEPARATOR . $project->machineName; + $serialized->id = $this->pluginId . '/' . $project->id; return $serialized; }; $values['list'] = array_map($map, $values['list']); diff --git a/src/Routing/ProjectBrowserRoutes.php b/src/Routing/ProjectBrowserRoutes.php index 6476426c7..4bbb15db4 100644 --- a/src/Routing/ProjectBrowserRoutes.php +++ b/src/Routing/ProjectBrowserRoutes.php @@ -46,97 +46,80 @@ class ProjectBrowserRoutes implements ContainerInjectionInterface { return []; } $routes = []; - $stage_id_regex = $machine_name_regex = '[a-zA-Z0-9_-]+'; $routes['project_browser.stage.begin'] = new Route( - '/admin/modules/project_browser/install-begin/{composer_namespace}/{project_id}', + '/admin/modules/project_browser/install-begin/{source}/{project_id}', [ '_controller' => InstallerController::class . '::begin', '_title' => 'Create phase', ], [ '_permission' => 'administer modules', - 'composer_namespace' => $machine_name_regex, - 'project_id' => $machine_name_regex, '_custom_access' => InstallerController::class . '::access', ], ); $routes['project_browser.stage.require'] = new Route( - '/admin/modules/project_browser/install-require/{composer_namespace}/{project_id}/{stage_id}', + '/admin/modules/project_browser/install-require/{source}/{project_id}', [ '_controller' => InstallerController::class . '::require', '_title' => 'Require phase', ], [ '_permission' => 'administer modules', - 'composer_namespace' => $machine_name_regex, - 'project_id' => $machine_name_regex, - 'stage_id' => $stage_id_regex, '_custom_access' => InstallerController::class . '::access', ], ); $routes['project_browser.stage.apply'] = new Route( - '/admin/modules/project_browser/install-apply/{composer_namespace}/{project_id}/{stage_id}', + '/admin/modules/project_browser/install-apply', [ '_controller' => InstallerController::class . '::apply', '_title' => 'Apply phase', ], [ '_permission' => 'administer modules', - 'composer_namespace' => $machine_name_regex, - 'project_id' => $machine_name_regex, - 'stage_id' => $stage_id_regex, '_custom_access' => InstallerController::class . '::access', ], ); $routes['project_browser.stage.post_apply'] = new Route( - '/admin/modules/project_browser/install-post_apply/{composer_namespace}/{project_id}/{stage_id}', + '/admin/modules/project_browser/install-post_apply', [ '_controller' => InstallerController::class . '::postApply', '_title' => 'Post apply phase', ], [ '_permission' => 'administer modules', - 'composer_namespace' => $machine_name_regex, - 'project_id' => $machine_name_regex, - 'stage_id' => $stage_id_regex, '_custom_access' => InstallerController::class . '::access', ], ); $routes['project_browser.stage.destroy'] = new Route( - '/admin/modules/project_browser/install-destroy/{composer_namespace}/{project_id}/{stage_id}', + '/admin/modules/project_browser/install-destroy', [ '_controller' => InstallerController::class . '::destroy', '_title' => 'Destroy phase', ], [ '_permission' => 'administer modules', - 'composer_namespace' => $machine_name_regex, - 'project_id' => $machine_name_regex, - 'stage_id' => $stage_id_regex, '_custom_access' => InstallerController::class . '::access', ], ); - $routes['project_browser.activate.module'] = new Route( - '/admin/modules/project_browser/activate-module/{project_id}', + $routes['project_browser.activate'] = new Route( + '/admin/modules/project_browser/activate/{source}/{project_id}', [ - '_controller' => InstallerController::class . '::activateModule', + '_controller' => InstallerController::class . '::activate', '_title' => 'Install module in core', ], [ '_permission' => 'administer modules', - 'project_id' => $machine_name_regex, '_custom_access' => InstallerController::class . '::access', ], ); $routes['project_browser.module.install_in_progress'] = new Route( - '/admin/modules/project_browser/install_in_progress/{project_id}', + '/admin/modules/project_browser/install_in_progress/{source}/{project_id}', [ '_controller' => InstallerController::class . '::inProgress', '_title' => 'Install in progress', ], [ '_permission' => 'administer modules', - 'project_id' => $machine_name_regex, '_custom_access' => InstallerController::class . '::access', ], ); diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js index 71bda0301671ab16e54561ecfb29b3507f95c9d5..87c244f65bde301946546c9c4104f3fb7abfe152 100644 GIT binary patch delta 141 zcmcchRAl>Ok%kt=Elh1Q*$NVqvlG*&Tg+xs+rDEalaLCpzDjjLQGQlxa*1AM%JvU? znHX7FGE-_3rgtA=lAf+`m}%DZ%ZHhiw`(3@QetGxobGju$(c2=pdhDmy3a8tN3MeW p;*xkc--9t>a^r2$>8{6_<hOr1#^lh-l$p}*=EJ<*&4*>mGXOI-HhKU6 delta 386 zcmdn~Smf4Ik%kt=Elh1Q1(Wl03-XIoi{kSVb5n~85|g)2oXKRY!l9d+pHiBWsy|)+ z53~I8OM97EMKF}6)+S8PIm9GAeePi<j_E%SF-?+8&d)0@QAkQn&&=}#vJ!K0yo&Sl z6l@iycOPa_o_ygen>HsWL=jLnJ=HS>D2J|G4=fd*nX-M&VI~zu87vm-t5ieuPrrDC z$wM8;Ehx&*N=+`&Bc#}Qdel)SE|PQ@rlb~^6y;YEX-i6N!sN!=qSH-|G0AU#c$CSZ Vb-LysX3=&PZ|3bP-Yk=!0RT$rm>>WE diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map index d9f70bd0776fc52e39e0792a88d8224c3f27e5e8..a75b81a7d650a3977aae6efc31ad4e1ed457f979 100644 GIT binary patch delta 524 zcmX@oBD}XjxS@q{3sVjgTR~!Sc4GQ;T^1&d?F*QgocMY5RjLb$^0QKtOY|~RwsQzE ztzu!xOsP$n?jyz|J^i5=(+#GWyy*wcn8dfUNigv<GG<OUlw|UnE+xg}%vF$IToRvH zP>@pz;(KlHm15d1#FUxR9-+&$JwliH{B6-lN5@DVXGce8YalLh)k$}9bWESVfRjaR zdk7~>J}XDAcbbknP|@@WOPNKtKj35e%EV&srSCj_{%U5a?Vkl%rn49tIqQ@I&2ZFl z1rh#^j{aaHL0nf5%Mn3XM>;xJxNhGo#v;!wiA_Gy(YeZP`gci|=K4TK*9<2eUq{Da zoqV9P@|_^2dOA9Kf}G@N4FbuI-Z?HhU~hoMfaWLZggQEgLhRIrxhvZRr^7&Q0(u>x z$poy)0Yrl2!jPmgU0~ipm$86*6yz*VM^CTqx-u-!nV1c|T&Hi0VUd^~@PL_PySY5e bW_}is&!<0}!z{rX=@^{nww+g>#e6aV0Ueae delta 859 zcma)(L1+^}6o$#{OBy33Nt>ieizHYDHIt-JL?jk`*)+>G*iuRl@z6Bgtg*>vcTFlH zQm9bqrJgE05Kp~%5foug-aLy^1i_0Z3*LJa+)b(;tg!<FZ)X1Y{XcKsZjHX%9^KmZ zykQ0lYgXH~x_YN-*4ld4u4%-9O^RoX6@vau+p6C;^c)3&CB^qJdjXNZ8uZOo%EZ_N zy^gWZR4p;zUU~m)s-dg()J$qkZ>*cD+1;oa#$4Ajvrg5}tLt^KGse=M)CsuUg-1P+ zTHjpdRE;yt)@<8&=-k^KtEp=n*}+}(BhGH6Pc1(-p(hD8bota>;%S135lR4y?#y#0 z=DGC#OQ=;qY!$eSDCeDhRb27Iu2-_kcW7z`BJ|-v@`;0CIAU;pE+-S-gmLjL1O(73 z@-#h;I--d)5TL0Q7z#KX%vVruD&l1nk`lc-l%n)C4qm^)Eyv3$P%;Biip3yE4}uV; z^9fMG8qVI8dBYJj3S4m@WF}yYc1Iv^TSM6>@&&{N_eJ@oawGFa#6>qe=G~=X*_3$& zvC?DYRg~9w*?H|r_^ewkBQCqg-{~(Nk3dCK`z|i84|M=drAMIsIHYe3mgY+TGP_sg zE&9kI@*jfTHyadpazF-W1S+c2C2?sS4w+|KgwDaI;GgfI=SR}8w}ML@MffssZSN-w C7zAJd diff --git a/sveltejs/src/Project/ActionButton.svelte b/sveltejs/src/Project/ActionButton.svelte index e92b4627c..3e9feb7b8 100644 --- a/sveltejs/src/Project/ActionButton.svelte +++ b/sveltejs/src/Project/ActionButton.svelte @@ -67,7 +67,7 @@ * Return is not used, but is a promise due to this being async. */ const showStatus = async (initiate = false) => { - const url = `${ORIGIN_URL}/admin/modules/project_browser/install_in_progress/${project.project_machine_name}`; + const url = `${ORIGIN_URL}/admin/modules/project_browser/install_in_progress/${project.id}`; // /** diff --git a/sveltejs/src/Project/AddInstallButton.svelte b/sveltejs/src/Project/AddInstallButton.svelte index 0e2844cb5..615395be7 100644 --- a/sveltejs/src/Project/AddInstallButton.svelte +++ b/sveltejs/src/Project/AddInstallButton.svelte @@ -60,7 +60,7 @@ */ async function installModule() { loading = true; - const url = `${ORIGIN_URL}/admin/modules/project_browser/activate-module/${project.project_machine_name}`; + const url = `${ORIGIN_URL}/admin/modules/project_browser/activate/${project.id}`; const installResponse = await fetch(url); if (!installResponse.ok) { handleError(installResponse); @@ -98,22 +98,21 @@ */ async function doRequests() { loading = true; - const beginInstallUrl = `${ORIGIN_URL}/admin/modules/project_browser/install-begin/${project.composer_namespace}`; + const beginInstallUrl = `${ORIGIN_URL}/admin/modules/project_browser/install-begin/${project.id}`; const beginInstallResponse = await fetch(beginInstallUrl); if (!beginInstallResponse.ok) { await handleError(beginInstallResponse); } else { const beginInstallJson = await beginInstallResponse.json(); - const stageId = beginInstallJson.stage_id; // The process of adding a module is separated into four stages, each // with their own endpoint. When one stage completes, the next one is // requested. const installSteps = [ - `${ORIGIN_URL}/admin/modules/project_browser/install-require/${project.composer_namespace}/${stageId}`, - `${ORIGIN_URL}/admin/modules/project_browser/install-apply/${project.composer_namespace}/${stageId}`, - `${ORIGIN_URL}/admin/modules/project_browser/install-post_apply/${project.composer_namespace}/${stageId}`, - `${ORIGIN_URL}/admin/modules/project_browser/install-destroy/${project.composer_namespace}/${stageId}`, + `${ORIGIN_URL}/admin/modules/project_browser/install-require/${project.id}`, + `${ORIGIN_URL}/admin/modules/project_browser/install-apply`, + `${ORIGIN_URL}/admin/modules/project_browser/install-post_apply`, + `${ORIGIN_URL}/admin/modules/project_browser/install-destroy`, ]; // eslint-disable-next-line no-restricted-syntax,guard-for-in diff --git a/sveltejs/src/popup.js b/sveltejs/src/popup.js index c6d80261a..4c716eda4 100644 --- a/sveltejs/src/popup.js +++ b/sveltejs/src/popup.js @@ -111,7 +111,7 @@ export const getCommandsPopupMessage = (project) => { <p>${composerText}</p> <p>${composerExistsText}:</p> <div class="command-box"> - <input value="composer require ${project.composer_namespace}" readonly/> + <input value="composer require ${project.package_name}" readonly/> ${downloadCopyButton} </div> diff --git a/tests/modules/project_browser_test/project_browser_test.services.yml b/tests/modules/project_browser_test/project_browser_test.services.yml index 91638f0b9..b01467dcf 100644 --- a/tests/modules/project_browser_test/project_browser_test.services.yml +++ b/tests/modules/project_browser_test/project_browser_test.services.yml @@ -8,3 +8,9 @@ services: tags: - { name: http_client_middleware } arguments: ['@module_handler'] + Drupal\project_browser_test\TestActivator: + autowire: true + public: false + decorates: 'Drupal\project_browser\ActivatorInterface' + arguments: + - '@.inner' diff --git a/tests/modules/project_browser_test/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php b/tests/modules/project_browser_test/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php index 51048153a..d3ef9eb2e 100644 --- a/tests/modules/project_browser_test/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php +++ b/tests/modules/project_browser_test/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php @@ -393,7 +393,7 @@ class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase { author: [ 'name' => $related[$uid_info['type']][$uid_info['id']]['name'], ], - composerNamespace: $project['attributes']['field_composer_namespace'] ?? 'drupal/' . $machine_name, + packageName: $project['attributes']['field_composer_namespace'] ?? 'drupal/' . $machine_name, categories: $module_categories, images: $project_images, ); diff --git a/tests/modules/project_browser_test/src/TestActivator.php b/tests/modules/project_browser_test/src/TestActivator.php new file mode 100644 index 000000000..fcc01dc98 --- /dev/null +++ b/tests/modules/project_browser_test/src/TestActivator.php @@ -0,0 +1,44 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\project_browser_test; + +use Drupal\Core\State\StateInterface; +use Drupal\project_browser\ActivatorInterface; +use Drupal\project_browser\ProjectBrowser\Project; +use Symfony\Component\HttpFoundation\Response; + +/** + * A test activator that simply logs a state message. + */ +class TestActivator implements ActivatorInterface { + + public function __construct( + private readonly ActivatorInterface $decorated, + private readonly StateInterface $state, + ) {} + + /** + * {@inheritdoc} + */ + public function supports(Project $project): bool { + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function isActive(Project $project): bool { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function activate(Project $project): ?Response { + $this->state->set('test activator', "$project->title was activated!"); + return $this->decorated->activate($project); + } + +} diff --git a/tests/src/Functional/InstallerControllerTest.php b/tests/src/Functional/InstallerControllerTest.php index ee5c09b15..bce90909c 100644 --- a/tests/src/Functional/InstallerControllerTest.php +++ b/tests/src/Functional/InstallerControllerTest.php @@ -99,7 +99,11 @@ class InstallerControllerTest extends BrowserTestBase { 'field_security_advisory_coverage' => 'covered', 'flag_project_star_user_count' => 0, 'field_project_type' => 'full', - 'project_data' => serialize([]), + 'project_data' => serialize([ + 'body' => [ + 'value' => $this->getRandomGenerator()->paragraphs(1), + ], + ]), 'field_project_machine_name' => 'awesome_module', ]); $query->values([ @@ -115,9 +119,33 @@ class InstallerControllerTest extends BrowserTestBase { 'field_security_advisory_coverage' => 'covered', 'flag_project_star_user_count' => 0, 'field_project_type' => 'full', - 'project_data' => serialize([]), + 'project_data' => serialize([ + 'body' => [ + 'value' => $this->getRandomGenerator()->paragraphs(1), + ], + ]), 'field_project_machine_name' => 'awesome_module', ]); + $query->values([ + 'nid' => 333, + 'title' => 'Drupal core', + 'author' => 'The usual gang of geniuses', + 'created' => 1383917647, + 'changed' => 1663534145, + 'project_usage_total' => 987654321, + 'maintenance_status' => 13028, + 'development_status' => 9988, + 'status' => 1, + 'field_security_advisory_coverage' => 'covered', + 'flag_project_star_user_count' => 0, + 'field_project_type' => 'full', + 'project_data' => serialize([ + 'body' => [ + 'value' => $this->getRandomGenerator()->paragraphs(1), + ], + ]), + 'field_project_machine_name' => 'core', + ]); $query->execute(); $this->initPackageManager(); $this->sharedTempStore = $this->container->get('tempstore.shared'); @@ -133,7 +161,7 @@ class InstallerControllerTest extends BrowserTestBase { */ public function testUiInstallUnavailableIfDisabled() { $this->config('project_browser.admin_settings')->set('allow_ui_install', FALSE)->save(); - $this->drupalGet('admin/modules/project_browser/install-begin/drupal/awesome_module'); + $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/awesome_module'); $this->assertSession()->statusCodeEquals(403); $this->assertSession()->pageTextContains('Access denied'); } @@ -145,7 +173,7 @@ class InstallerControllerTest extends BrowserTestBase { */ public function testInstallSecurityRevokedModule() { $this->assertProjectBrowserTempStatus(NULL, NULL); - $content = $this->drupalGet('admin/modules/project_browser/install-begin/drupal/security_revoked_module'); + $content = $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/security_revoked_module'); $this->assertSession()->statusCodeEquals(500); $this->assertSame('{"message":"security_revoked_module is not safe to add because its security coverage has been revoked"}', $content); } @@ -160,9 +188,9 @@ class InstallerControllerTest extends BrowserTestBase { // Though core is not available as a choice in project browser, it works // well for the purposes of this test as it's definitely already added // via composer. - $this->drupalGet('admin/modules/project_browser/install-begin/drupal/core'); + $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/core'); $this->stageId = $this->sharedTempStore->get('package_manager_stage')->get('lock')[0]; - $content = $this->drupalGet("/admin/modules/project_browser/install-require/drupal/core/$this->stageId"); + $content = $this->drupalGet("/admin/modules/project_browser/install-require/drupalorg_mockapi/core"); $this->assertSession()->statusCodeEquals(500); $this->assertSame('{"message":"StageEventException: The following package is already installed: drupal\/core\n","phase":"require"}', $content); } @@ -174,12 +202,12 @@ class InstallerControllerTest extends BrowserTestBase { */ private function doStart() { $this->assertProjectBrowserTempStatus(NULL, NULL); - $this->drupalGet('admin/modules/project_browser/install-begin/drupal/awesome_module'); + $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/awesome_module'); $this->stageId = $this->sharedTempStore->get('package_manager_stage')->get('lock')[0]; $this->assertSession()->statusCodeEquals(200); $expected_output = sprintf('{"phase":"create","status":0,"stage_id":"%s"}', $this->stageId); $this->assertSame($expected_output, $this->getSession()->getPage()->getContent()); - $this->assertInstallInProgress('awesome_module', 'creating install stage'); + $this->assertInstallInProgress('drupalorg_mockapi/awesome_module', 'creating install stage'); } /** @@ -188,10 +216,10 @@ class InstallerControllerTest extends BrowserTestBase { * @covers ::require */ private function doRequire() { - $this->drupalGet("/admin/modules/project_browser/install-require/drupal/awesome_module/$this->stageId"); + $this->drupalGet("/admin/modules/project_browser/install-require/drupalorg_mockapi/awesome_module"); $expected_output = sprintf('{"phase":"require","status":0,"stage_id":"%s"}', $this->stageId); $this->assertSame($expected_output, $this->getSession()->getPage()->getContent()); - $this->assertInstallInProgress('awesome_module', 'requiring module'); + $this->assertInstallInProgress('drupalorg_mockapi/awesome_module', 'requiring module'); } /** @@ -200,10 +228,10 @@ class InstallerControllerTest extends BrowserTestBase { * @covers ::apply */ private function doApply() { - $this->drupalGet("/admin/modules/project_browser/install-apply/drupal/awesome_module/$this->stageId"); + $this->drupalGet("/admin/modules/project_browser/install-apply"); $expected_output = sprintf('{"phase":"apply","status":0,"stage_id":"%s"}', $this->stageId); $this->assertSame($expected_output, $this->getSession()->getPage()->getContent()); - $this->assertInstallInProgress('awesome_module', 'applying'); + $this->assertInstallInProgress('drupalorg_mockapi/awesome_module', 'applying'); } /** @@ -212,10 +240,10 @@ class InstallerControllerTest extends BrowserTestBase { * @covers ::postApply */ private function doPostApply() { - $this->drupalGet("/admin/modules/project_browser/install-post_apply/drupal/awesome_module/$this->stageId"); + $this->drupalGet("/admin/modules/project_browser/install-post_apply"); $expected_output = sprintf('{"phase":"post apply","status":0,"stage_id":"%s"}', $this->stageId); $this->assertSame($expected_output, $this->getSession()->getPage()->getContent()); - $this->assertInstallInProgress('awesome_module', 'post apply'); + $this->assertInstallInProgress('drupalorg_mockapi/awesome_module', 'post apply'); } /** @@ -224,7 +252,7 @@ class InstallerControllerTest extends BrowserTestBase { * @covers ::destroy */ private function doDestroy() { - $this->drupalGet("/admin/modules/project_browser/install-destroy/drupal/awesome_module/$this->stageId"); + $this->drupalGet("/admin/modules/project_browser/install-destroy"); $expected_output = sprintf('{"phase":"destroy","status":0,"stage_id":"%s"}', $this->stageId); $this->assertSame($expected_output, $this->getSession()->getPage()->getContent()); $this->assertInstallNotInProgress('awesome_module'); @@ -250,7 +278,7 @@ class InstallerControllerTest extends BrowserTestBase { $message = t('This is a PreCreate error.'); $result = ValidationResult::createError([$message]); TestSubscriber::setTestResult([$result], PreCreateEvent::class); - $contents = $this->drupalGet('admin/modules/project_browser/install-begin/drupal/awesome_module'); + $contents = $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/awesome_module'); $this->assertSession()->statusCodeEquals(500); $this->assertSame('{"message":"StageEventException: This is a PreCreate error.\n","phase":"create"}', $contents); } @@ -263,7 +291,7 @@ class InstallerControllerTest extends BrowserTestBase { public function testPreCreateException() { $error = new \Exception('PreCreate did not go well.'); TestSubscriber::setException($error, PreCreateEvent::class); - $contents = $this->drupalGet('admin/modules/project_browser/install-begin/drupal/awesome_module'); + $contents = $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/awesome_module'); $this->assertSession()->statusCodeEquals(500); $this->assertSame('{"message":"StageEventException: PreCreate did not go well.","phase":"create"}', $contents); } @@ -276,7 +304,7 @@ class InstallerControllerTest extends BrowserTestBase { public function testPostCreateException() { $error = new \Exception('PostCreate did not go well.'); TestSubscriber::setException($error, PostCreateEvent::class); - $contents = $this->drupalGet('admin/modules/project_browser/install-begin/drupal/awesome_module'); + $contents = $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/awesome_module'); $this->assertSession()->statusCodeEquals(500); $this->assertSame('{"message":"StageEventException: PostCreate did not go well.","phase":"create"}', $contents); } @@ -291,7 +319,7 @@ class InstallerControllerTest extends BrowserTestBase { $result = ValidationResult::createError([$message]); $this->doStart(); TestSubscriber::setTestResult([$result], PreRequireEvent::class); - $contents = $this->drupalGet("/admin/modules/project_browser/install-require/drupal/awesome_module/$this->stageId"); + $contents = $this->drupalGet("/admin/modules/project_browser/install-require/drupalorg_mockapi/awesome_module"); $this->assertSession()->statusCodeEquals(500); $this->assertSame('{"message":"StageEventException: This is a PreRequire error.\n","phase":"require"}', $contents); } @@ -305,7 +333,7 @@ class InstallerControllerTest extends BrowserTestBase { $error = new \Exception('PreRequire did not go well.'); TestSubscriber::setException($error, PreRequireEvent::class); $this->doStart(); - $contents = $this->drupalGet("/admin/modules/project_browser/install-require/drupal/awesome_module/$this->stageId"); + $contents = $this->drupalGet("/admin/modules/project_browser/install-require/drupalorg_mockapi/awesome_module"); $this->assertSession()->statusCodeEquals(500); $this->assertSame('{"message":"StageEventException: PreRequire did not go well.","phase":"require"}', $contents); } @@ -319,7 +347,7 @@ class InstallerControllerTest extends BrowserTestBase { $error = new \Exception('PostRequire did not go well.'); TestSubscriber::setException($error, PostRequireEvent::class); $this->doStart(); - $contents = $this->drupalGet("/admin/modules/project_browser/install-require/drupal/awesome_module/$this->stageId"); + $contents = $this->drupalGet("/admin/modules/project_browser/install-require/drupalorg_mockapi/awesome_module"); $this->assertSession()->statusCodeEquals(500); $this->assertSame('{"message":"StageEventException: PostRequire did not go well.","phase":"require"}', $contents); } @@ -335,7 +363,7 @@ class InstallerControllerTest extends BrowserTestBase { TestSubscriber::setTestResult([$result], PreApplyEvent::class); $this->doStart(); $this->doRequire(); - $contents = $this->drupalGet("/admin/modules/project_browser/install-apply/drupal/awesome_module/$this->stageId"); + $contents = $this->drupalGet("/admin/modules/project_browser/install-apply"); $this->assertSession()->statusCodeEquals(500); $this->assertSame('{"message":"StageEventException: This is a PreApply error.\n","phase":"apply"}', $contents); } @@ -350,7 +378,7 @@ class InstallerControllerTest extends BrowserTestBase { TestSubscriber::setException($error, PreApplyEvent::class); $this->doStart(); $this->doRequire(); - $contents = $this->drupalGet("/admin/modules/project_browser/install-apply/drupal/awesome_module/$this->stageId"); + $contents = $this->drupalGet("/admin/modules/project_browser/install-apply"); $this->assertSession()->statusCodeEquals(500); $this->assertSame('{"message":"StageEventException: PreApply did not go well.","phase":"apply"}', $contents); } @@ -366,7 +394,7 @@ class InstallerControllerTest extends BrowserTestBase { $this->doStart(); $this->doRequire(); $this->doApply(); - $contents = $this->drupalGet("/admin/modules/project_browser/install-post_apply/drupal/awesome_module/$this->stageId"); + $contents = $this->drupalGet("/admin/modules/project_browser/install-post_apply"); $this->assertSession()->statusCodeEquals(500); $this->assertSame('{"message":"StageEventException: PostApply did not go well.","phase":"post apply"}', $contents); } @@ -380,37 +408,37 @@ class InstallerControllerTest extends BrowserTestBase { $this->doStart(); // Check for mid install unlock offer message. - $this->drupalGet('admin/modules/project_browser/install-begin/drupal/awesome_module'); + $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/awesome_module'); $this->assertSession()->statusCodeEquals(418); $this->assertMatchesRegularExpression('/{"message":"The install staging area was locked less than 1 minutes ago. This is recent enough that a legitimate installation may be in progress. Consider waiting before unlocking the installation staging area.","unlock_url":".*admin..modules..project_browser..install..unlock\?token=[a-zA-Z0-9_-]*"}/', $this->getSession()->getPage()->getContent()); - $this->assertInstallInProgress('awesome_module', 'creating install stage'); + $this->assertInstallInProgress('drupalorg_mockapi/awesome_module', 'creating install stage'); $this->assertFalse($this->installer->isAvailable()); $this->assertFalse($this->installer->isApplying()); TestTime::setFakeTimeByOffset("+800 seconds"); - $this->drupalGet('admin/modules/project_browser/install-begin/drupal/awesome_module'); + $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/awesome_module'); $this->assertSession()->statusCodeEquals(418); $this->assertFalse($this->installer->isAvailable()); $this->assertFalse($this->installer->isApplying()); $this->assertMatchesRegularExpression('/{"message":"The install staging area was locked 13 minutes ago.","unlock_url":".*admin..modules..project_browser..install..unlock\?token=[a-zA-Z0-9_-]*"}/', $this->getSession()->getPage()->getContent()); $this->doRequire(); - $this->drupalGet('admin/modules/project_browser/install-begin/drupal/awesome_module'); + $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/awesome_module'); $this->assertSession()->statusCodeEquals(418); $this->assertFalse($this->installer->isAvailable()); $this->assertFalse($this->installer->isApplying()); $this->doApply(); TestTime::setFakeTimeByOffset('+800 seconds'); - $this->drupalGet('admin/modules/project_browser/install-begin/drupal/awesome_module'); + $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/awesome_module'); $this->assertSession()->statusCodeEquals(418); $this->assertFalse($this->installer->isAvailable()); $this->assertTrue($this->installer->isApplying()); $this->assertMatchesRegularExpression('/{"message":"The install staging area was locked 13 minutes ago. It should not be unlocked as the changes from staging are being applied to the site.","unlock_url":""}/', $this->getSession()->getPage()->getContent()); TestTime::setFakeTimeByOffset("+55 minutes"); - $this->drupalGet('admin/modules/project_browser/install-begin/drupal/awesome_module'); + $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/awesome_module'); $this->assertSession()->statusCodeEquals(418); $this->assertMatchesRegularExpression('/{"message":"The install staging area was locked 55 minutes ago. It should not be unlocked as the changes from staging are being applied to the site.","unlock_url":""}/', $this->getSession()->getPage()->getContent()); // Unlocking the stage becomes possible after 1 hour regardless of source. TestTime::setFakeTimeByOffset("+75 minutes"); - $this->drupalGet('admin/modules/project_browser/install-begin/drupal/awesome_module'); + $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/awesome_module'); $this->assertSession()->statusCodeEquals(418); $this->assertMatchesRegularExpression('/{"message":"The install staging area was locked 1 hours, 15 minutes ago.","unlock_url":".*admin..modules..project_browser..install..unlock\?token=[a-zA-Z0-9_-]*"}/', $this->getSession()->getPage()->getContent()); } @@ -426,7 +454,7 @@ class InstallerControllerTest extends BrowserTestBase { $this->doStart(); // Try beginning another install while one is in progress, but not yet in // the applying stage. - $content = $this->drupalGet('admin/modules/project_browser/install-begin/drupal/metatag'); + $content = $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/metatag'); $this->assertSession()->statusCodeEquals(418); $this->assertFalse($this->installer->isAvailable()); $this->assertFalse($this->installer->isApplying()); @@ -450,7 +478,7 @@ class InstallerControllerTest extends BrowserTestBase { public function testCanBreakStageWithMissingProjectBrowserLock() { $this->doStart(); $this->sharedTempStore->get('project_browser')->delete('requiring'); - $content = $this->drupalGet('admin/modules/project_browser/install-begin/drupal/metatag'); + $content = $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/metatag'); $this->assertSession()->statusCodeEquals(418); $this->assertFalse($this->installer->isAvailable()); $this->assertFalse($this->installer->isApplying()); @@ -471,21 +499,25 @@ class InstallerControllerTest extends BrowserTestBase { * * @covers ::activateModule */ - public function testCoreModuleActivate() { + public function testCoreModuleActivate(): void { + $assert_session = $this->assertSession(); + + // Since we are activating a core module, we need the source plugin that + // exposes core modules to be enabled. + $this->config('project_browser.admin_settings') + ->set('enabled_sources', ['drupal_core']) + ->save(); + $this->drupalGet('admin/modules'); - $views_checkbox = $this->getSession()->getPage()->find('css', '#edit-modules-views-enable'); - $views_ui_checkbox = $this->getSession()->getPage()->find('css', '#edit-modules-views-ui-enable'); - $this->assertFalse($views_checkbox->isChecked()); - $this->assertFalse($views_ui_checkbox->isChecked()); + $assert_session->checkboxNotChecked('edit-modules-views-enable'); + $assert_session->checkboxNotChecked('edit-modules-views-ui-enable'); - $content = $this->drupalGet('admin/modules/project_browser/activate-module/views_ui'); + $content = $this->drupalGet('admin/modules/project_browser/activate/drupal_core/views_ui'); $this->assertSame('{"status":0}', $content); $this->rebuildContainer(); $this->drupalGet('admin/modules'); - $views_checkbox = $this->getSession()->getPage()->find('css', '#edit-modules-views-enable'); - $views_ui_checkbox = $this->getSession()->getPage()->find('css', '#edit-modules-views-ui-enable'); - $this->assertTrue($views_checkbox->isChecked()); - $this->assertTrue($views_ui_checkbox->isChecked()); + $assert_session->checkboxChecked('edit-modules-views-enable'); + $assert_session->checkboxChecked('edit-modules-views-ui-enable'); } /** @@ -496,9 +528,9 @@ class InstallerControllerTest extends BrowserTestBase { */ protected function assertInstallNotInProgress($module) { $this->assertProjectBrowserTempStatus(NULL, NULL); - $this->drupalGet("/admin/modules/project_browser/install_in_progress/$module"); + $this->drupalGet("/admin/modules/project_browser/install_in_progress/drupalorg_mockapi/$module"); $this->assertSame('{"status":0}', $this->getSession()->getPage()->getContent()); - $this->drupalGet('/admin/modules/project_browser/install_in_progress/metatag'); + $this->drupalGet('/admin/modules/project_browser/install_in_progress/drupalorg_mockapi/metatag'); $this->assertSame('{"status":0}', $this->getSession()->getPage()->getContent()); } @@ -521,7 +553,7 @@ class InstallerControllerTest extends BrowserTestBase { $this->assertProjectBrowserTempStatus($expect_install, NULL); $this->drupalGet("/admin/modules/project_browser/install_in_progress/$module"); $this->assertSame(sprintf('{"status":1,"phase":"%s"}', $phase), $this->getSession()->getPage()->getContent()); - $this->drupalGet('/admin/modules/project_browser/install_in_progress/metatag'); + $this->drupalGet('/admin/modules/project_browser/install_in_progress/drupalorg_mockapi/metatag'); $this->assertSame('{"status":0}', $this->getSession()->getPage()->getContent()); } @@ -536,6 +568,18 @@ class InstallerControllerTest extends BrowserTestBase { protected function assertProjectBrowserTempStatus($expected_requiring, $expected_installing) { $project_browser_requiring = $this->sharedTempStore->get('project_browser')->get('requiring'); $project_browser_installing = $this->sharedTempStore->get('project_browser')->get('installing'); + if (is_array($expected_installing)) { + ksort($expected_installing); + } + if (is_array($expected_requiring)) { + ksort($expected_requiring); + } + if (is_array($project_browser_installing)) { + ksort($project_browser_installing); + } + if (is_array($project_browser_requiring)) { + ksort($project_browser_requiring); + } $this->assertSame($expected_requiring, $project_browser_requiring); $this->assertSame($expected_installing, $project_browser_installing); } diff --git a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php index 4d278eaf2..466f888e4 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Drupal\Tests\project_browser\FunctionalJavascript; +use Drupal\Core\State\StateInterface; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\Tests\project_browser\Traits\PackageManagerFixtureUtilityTrait; @@ -69,8 +70,12 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { $this->assertSame('Add and Install Cream cheese on a bagel', $download_button->getText()); $download_button->click(); $installed_action = $assert_session->waitForElementVisible('css', "$cream_cheese_module_selector .project_status-indicator", 30000); - $assert_session->waitForText('✓ Cream cheese on a bagel is Installed'); + $this->assertTrue($assert_session->waitForText('✓ Cream cheese on a bagel is Installed')); $this->assertSame('✓ Cream cheese on a bagel is Installed', $installed_action->getText()); + + // The activator in project_browser_test should have logged a message. + // @see \Drupal\project_browser_test\TestActivator + $this->assertSame('Cream cheese on a bagel was activated!', $this->container->get(StateInterface::class)->get('test activator')); } /** @@ -144,7 +149,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { $page = $this->getSession()->getPage(); // Start install begin. - $this->drupalGet('admin/modules/project_browser/install-begin/drupal/metatag'); + $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/metatag'); $this->sharedTempStore->get('project_browser')->delete('requiring'); $this->drupalGet('admin/modules/browse'); $this->svelteInitHelper('text', 'Cream cheese on a bagel'); @@ -180,7 +185,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { $page = $this->getSession()->getPage(); // Start install begin. - $this->drupalGet('admin/modules/project_browser/install-begin/drupal/metatag'); + $this->drupalGet('admin/modules/project_browser/install-begin/drupalorg_mockapi/metatag'); $this->drupalGet('admin/modules/browse'); $this->svelteInitHelper('text', 'Cream cheese on a bagel'); // Try beginning another install while one is in progress, but not yet in @@ -198,7 +203,6 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { $installed_action = $assert_session->waitForElementVisible('css', "$cream_cheese_module_selector .project_status-indicator", 30000); $assert_session->waitForText('✓ Cream cheese on a bagel is Installed'); $this->assertSame('✓ Cream cheese on a bagel is Installed', $installed_action->getText()); - } } -- GitLab