Skip to content
Snippets Groups Projects
Commit 7a864968 authored by Narendra Singh Rathore's avatar Narendra Singh Rathore Committed by Chris Wells
Browse files

Issue #3479219: Replace InstallerController's tempstore with a centralized InstallState service

parent 5284ee04
No related branches found
No related tags found
No related merge requests found
...@@ -6,13 +6,12 @@ use Drupal\Component\Datetime\TimeInterface; ...@@ -6,13 +6,12 @@ use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Utility\DeprecationHelper; use Drupal\Component\Utility\DeprecationHelper;
use Drupal\Core\Access\AccessResult; use Drupal\Core\Access\AccessResult;
use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\TempStore\SharedTempStore;
use Drupal\Core\TempStore\SharedTempStoreFactory;
use Drupal\Core\Url; use Drupal\Core\Url;
use Drupal\package_manager\Exception\StageException; use Drupal\package_manager\Exception\StageException;
use Drupal\project_browser\ActivatorInterface; use Drupal\project_browser\ActivatorInterface;
use Drupal\project_browser\ComposerInstaller\Installer; use Drupal\project_browser\ComposerInstaller\Installer;
use Drupal\project_browser\EnabledSourceHandler; use Drupal\project_browser\EnabledSourceHandler;
use Drupal\project_browser\InstallState;
use Drupal\project_browser\ProjectBrowser\Project; use Drupal\project_browser\ProjectBrowser\Project;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
...@@ -53,39 +52,14 @@ class InstallerController extends ControllerBase { ...@@ -53,39 +52,14 @@ class InstallerController extends ControllerBase {
*/ */
protected const STAGE_STATUS_OK = 0; protected const STAGE_STATUS_OK = 0;
/**
* The Project Browser tempstore object.
*
* @var \Drupal\Core\TempStore\SharedTempStore
*/
protected SharedTempStore $projectBrowserTempStore;
/**
* Constructor for install controller.
*
* @param \Drupal\project_browser\ComposerInstaller\Installer $installer
* The installer service.
* @param \Drupal\Core\TempStore\SharedTempStoreFactory $shared_temp_store_factory
* The temporary storage factory.
* @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( public function __construct(
private readonly Installer $installer, private readonly Installer $installer,
SharedTempStoreFactory $shared_temp_store_factory,
private readonly EnabledSourceHandler $enabledSourceHandler, private readonly EnabledSourceHandler $enabledSourceHandler,
private readonly TimeInterface $time, private readonly TimeInterface $time,
private readonly LoggerInterface $logger, private readonly LoggerInterface $logger,
private readonly ActivatorInterface $activator, private readonly ActivatorInterface $activator,
) { private readonly InstallState $installState,
$this->projectBrowserTempStore = $shared_temp_store_factory->get('project_browser'); ) {}
}
/** /**
* {@inheritdoc} * {@inheritdoc}
...@@ -93,11 +67,11 @@ class InstallerController extends ControllerBase { ...@@ -93,11 +67,11 @@ class InstallerController extends ControllerBase {
public static function create(ContainerInterface $container) { public static function create(ContainerInterface $container) {
return new static( return new static(
$container->get(Installer::class), $container->get(Installer::class),
$container->get(SharedTempStoreFactory::class),
$container->get(EnabledSourceHandler::class), $container->get(EnabledSourceHandler::class),
$container->get(TimeInterface::class), $container->get(TimeInterface::class),
$container->get('logger.channel.project_browser'), $container->get('logger.channel.project_browser'),
$container->get(ActivatorInterface::class), $container->get(ActivatorInterface::class),
$container->get(InstallState::class),
); );
} }
...@@ -109,19 +83,11 @@ class InstallerController extends ControllerBase { ...@@ -109,19 +83,11 @@ class InstallerController extends ControllerBase {
return AccessResult::allowedIf((bool) $ui_install); return AccessResult::allowedIf((bool) $ui_install);
} }
/**
* Nulls the installing and core installing states.
*/
private function resetProgress(): void {
$this->projectBrowserTempStore->delete('requiring');
$this->projectBrowserTempStore->delete('installing');
}
/** /**
* Resets progress and destroys the stage. * Resets progress and destroys the stage.
*/ */
private function cancelRequire(): void { private function cancelRequire(): void {
$this->resetProgress(); $this->installState->deleteAll();
// Checking the for the presence of a lock in the package manager stage is // Checking the for the presence of a lock in the package manager stage is
// necessary as this method can be called during create(), which includes // necessary as this method can be called during create(), which includes
// both the PreCreate and PostCreate events. If an exception is caught // both the PreCreate and PostCreate events. If an exception is caught
...@@ -148,27 +114,17 @@ class InstallerController extends ControllerBase { ...@@ -148,27 +114,17 @@ class InstallerController extends ControllerBase {
* *
* @return \Symfony\Component\HttpFoundation\JsonResponse * @return \Symfony\Component\HttpFoundation\JsonResponse
* Information about the project's require/install status. * Information about the project's require/install status.
*
* If the project is being required, the response will include which require
* phase is currently occurring.
*
* When a project is required via the UI, the UI fetches this endpoint
* regularly so it can monitor the progress of the process and report which
* stage is taking place.
*/ */
public function inProgress(Project $project): JsonResponse { public function inProgress(Project $project): JsonResponse {
$requiring = $this->projectBrowserTempStore->get('requiring'); $project_state = $this->installState->getStatus($project);
$core_installing = $this->projectBrowserTempStore->get('installing');
$return = ['status' => self::STATUS_IDLE]; $return = ['status' => self::STATUS_IDLE];
if (isset($requiring['project_id']) && $requiring['project_id'] === $project->id) { if ($project_state !== NULL) {
$return['status'] = self::STATUS_REQUIRING_PROJECT; $return['status'] = ($project_state === 'requiring' || $project_state === 'applying')
$return['phase'] = $requiring['phase']; ? self::STATUS_REQUIRING_PROJECT
: self::STATUS_INSTALLING_PROJECT;
$return['phase'] = $project_state;
} }
if ($core_installing === $project->id) {
$return['status'] = self::STATUS_INSTALLING_PROJECT;
}
return new JsonResponse($return); return new JsonResponse($return);
} }
...@@ -242,24 +198,6 @@ class InstallerController extends ControllerBase { ...@@ -242,24 +198,6 @@ class InstallerController extends ControllerBase {
], 418); ], 418);
} }
/**
* Updates the 'requiring' state in the temp store.
*
* @param string $id
* The ID of the project being required, as known to the enabled sources
* handler.
* @param string $phase
* The require phase in progress.
*/
private function setRequiringState(?string $id, string $phase): void {
$data = $this->projectBrowserTempStore->get('requiring') ?? [];
if ($id) {
$data['project_id'] = $id;
}
$data['phase'] = $phase;
$this->projectBrowserTempStore->set('requiring', $data);
}
/** /**
* Unlocks and destroys the stage. * Unlocks and destroys the stage.
* *
...@@ -291,7 +229,7 @@ class InstallerController extends ControllerBase { ...@@ -291,7 +229,7 @@ class InstallerController extends ControllerBase {
catch (\Exception $e) { catch (\Exception $e) {
return $this->errorResponse($e); return $this->errorResponse($e);
} }
$this->projectBrowserTempStore->delete('requiring'); $this->installState->deleteAll();
$this->messenger()->addStatus($this->t('Install staging area unlocked.')); $this->messenger()->addStatus($this->t('Install staging area unlocked.'));
return $this->redirect('project_browser.browse'); return $this->redirect('project_browser.browse');
} }
...@@ -332,18 +270,18 @@ class InstallerController extends ControllerBase { ...@@ -332,18 +270,18 @@ class InstallerController extends ControllerBase {
public function begin(): JsonResponse { public function begin(): JsonResponse {
$stage_available = $this->installer->isAvailable(); $stage_available = $this->installer->isAvailable();
if (!$stage_available) { if (!$stage_available) {
$requiring_metadata = $this->projectBrowserTempStore->getMetadata('requiring'); $updated_time = $this->installState->getFirstUpdatedTime();
if (!$this->installer->lockCameFromProjectBrowserInstaller()) { if (!$this->installer->lockCameFromProjectBrowserInstaller()) {
return $this->lockedResponse($this->t('The installation stage is locked by a process outside of Project Browser'), ''); return $this->lockedResponse($this->t('The installation stage is locked by a process outside of Project Browser'), '');
} }
if (empty($requiring_metadata)) { if (empty($updated_time)) {
$unlock_url = self::getUrlWithReplacedCsrfTokenPlaceholder( $unlock_url = self::getUrlWithReplacedCsrfTokenPlaceholder(
Url::fromRoute('project_browser.install.unlock') Url::fromRoute('project_browser.install.unlock')
); );
$message = t('An install staging area claimed by Project Browser exists but has expired. You may unlock the stage and try the install again.'); $message = t('An install staging area claimed by Project Browser exists but has expired. You may unlock the stage and try the install again.');
return $this->lockedResponse($message, $unlock_url); return $this->lockedResponse($message, $unlock_url);
} }
$time_since_updated = $this->time->getRequestTime() - $requiring_metadata->getUpdated(); $time_since_updated = $this->time->getRequestTime() - $updated_time;
$hours = (int) gmdate("H", $time_since_updated); $hours = (int) gmdate("H", $time_since_updated);
$minutes = (int) gmdate("i", $time_since_updated); $minutes = (int) gmdate("i", $time_since_updated);
$minutes = $time_since_updated > 60 ? $minutes : 'less than 1'; $minutes = $time_since_updated > 60 ? $minutes : 'less than 1';
...@@ -400,7 +338,7 @@ class InstallerController extends ControllerBase { ...@@ -400,7 +338,7 @@ class InstallerController extends ControllerBase {
* Status message. * Status message.
*/ */
public function require(Request $request, string $stage_id): JsonResponse { public function require(Request $request, string $stage_id): JsonResponse {
$package_names = $package_ids = []; $package_names = [];
foreach ($request->toArray() as $project) { foreach ($request->toArray() as $project) {
$project = $this->enabledSourceHandler->getStoredProject($project); $project = $this->enabledSourceHandler->getStoredProject($project);
if ($project->source === 'project_browser_test_mock') { if ($project->source === 'project_browser_test_mock') {
...@@ -412,23 +350,11 @@ class InstallerController extends ControllerBase { ...@@ -412,23 +350,11 @@ class InstallerController extends ControllerBase {
return new JsonResponse(['message' => "$project->machineName is not safe to add because its security coverage has been revoked"], 500); return new JsonResponse(['message' => "$project->machineName is not safe to add because its security coverage has been revoked"], 500);
} }
} }
$this->installState->setState($project, 'requiring');
$package_names[] = $project->packageName; $package_names[] = $project->packageName;
$package_ids[] = $project->id;
}
$requiring = $this->projectBrowserTempStore->get('requiring');
$current_package_names = implode(', ', $package_names);
if (!empty($requiring['project_id']) && $requiring['project_id'] !== $current_package_names) {
$error_message = sprintf(
'Error: a request to install %s was ignored as an install for a different project is in progress.',
$current_package_names
);
return new JsonResponse(['message' => $error_message], 500);
} }
$this->setRequiringState(implode(', ', $package_ids), 'requiring module');
try { try {
$this->installer->claim($stage_id)->require($package_names); $this->installer->claim($stage_id)->require($package_names);
$this->setRequiringState(NULL, 'requiring module');
return $this->successResponse('require', $stage_id); return $this->successResponse('require', $stage_id);
} }
catch (\Exception $e) { catch (\Exception $e) {
...@@ -447,7 +373,9 @@ class InstallerController extends ControllerBase { ...@@ -447,7 +373,9 @@ class InstallerController extends ControllerBase {
* Status message. * Status message.
*/ */
public function apply(string $stage_id): JsonResponse { public function apply(string $stage_id): JsonResponse {
$this->setRequiringState(NULL, 'applying'); foreach (array_keys($this->installState->toArray()) as $project_id) {
$this->installState->setState($this->enabledSourceHandler->getStoredProject($project_id), 'applying');
}
try { try {
$this->installer->claim($stage_id)->apply(); $this->installer->claim($stage_id)->apply();
} }
...@@ -468,7 +396,6 @@ class InstallerController extends ControllerBase { ...@@ -468,7 +396,6 @@ class InstallerController extends ControllerBase {
* Status message. * Status message.
*/ */
public function postApply(string $stage_id): JsonResponse { public function postApply(string $stage_id): JsonResponse {
$this->setRequiringState(NULL, 'post apply');
try { try {
$this->installer->claim($stage_id)->postApply(); $this->installer->claim($stage_id)->postApply();
} }
...@@ -488,14 +415,12 @@ class InstallerController extends ControllerBase { ...@@ -488,14 +415,12 @@ class InstallerController extends ControllerBase {
* Status message. * Status message.
*/ */
public function destroy(string $stage_id): JsonResponse { public function destroy(string $stage_id): JsonResponse {
$this->setRequiringState(NULL, 'completing');
try { try {
$this->installer->claim($stage_id)->destroy(); $this->installer->claim($stage_id)->destroy();
} }
catch (\Exception $e) { catch (\Exception $e) {
return $this->errorResponse($e, 'destroy'); return $this->errorResponse($e, 'destroy');
} }
$this->projectBrowserTempStore->delete('requiring');
return new JsonResponse([ return new JsonResponse([
'phase' => 'destroy', 'phase' => 'destroy',
'status' => self::STAGE_STATUS_OK, 'status' => self::STAGE_STATUS_OK,
...@@ -515,15 +440,16 @@ class InstallerController extends ControllerBase { ...@@ -515,15 +440,16 @@ class InstallerController extends ControllerBase {
public function activate(Request $request): JsonResponse { public function activate(Request $request): JsonResponse {
foreach ($request->toArray() as $project) { foreach ($request->toArray() as $project) {
$project = $this->enabledSourceHandler->getStoredProject($project); $project = $this->enabledSourceHandler->getStoredProject($project);
$this->projectBrowserTempStore->set('installing', $project->id); $this->installState->setState($project, 'activating');
try { try {
$this->activator->activate($project); $this->activator->activate($project);
$this->installState->setState($project, 'installed');
} }
catch (\Throwable $e) { catch (\Throwable $e) {
return $this->errorResponse($e, 'project install'); return $this->errorResponse($e, 'project install');
} }
finally { finally {
$this->resetProgress(); $this->installState->deleteAll();
} }
} }
return new JsonResponse(['status' => 0]); return new JsonResponse(['status' => 0]);
......
<?php
namespace Drupal\project_browser;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
use Drupal\project_browser\ProjectBrowser\Project;
/**
* Defines a service to manage the installation state of projects.
*/
final class InstallState {
/**
* The key-value storage.
*/
private readonly KeyValueStoreInterface $keyValue;
public function __construct(
KeyValueFactoryInterface $keyValueFactory,
private readonly TimeInterface $time,
) {
$this->keyValue = $keyValueFactory->get('project_browser.install_status');
}
/**
* Returns information on all in-progress project installs and timestamp.
*
* @param bool $include_timestamp
* Whether to include the `__timestamp` entry in the returned array.
* Defaults to FALSE.
*
* @return array
* The array contains:
* - Project states: Keyed by project ID, where each entry is an associative
* array containing:
* - source: The source plugin ID for the project.
* - status: The installation status of the project, or NULL if not set.
* - A separate `__timestamp` entry: The UNIX timestamp indicating when the
* request started (included only if $include_timestamp is TRUE).
*
* Example return value:
* [
* 'project_id1' => [
* 'source' => 'source_plugin_id1',
* 'status' => 'requiring',
* ],
* 'project_id2' => [
* 'source' => 'source_plugin_id2',
* 'status' => 'installing',
* ],
* '__timestamp' => 1732086755,
* ]
*/
public function toArray(bool $include_timestamp = FALSE): array {
$data = $this->keyValue->getAll();
if (!$include_timestamp) {
unset($data['__timestamp']);
}
return $data;
}
/**
* Sets project state and initializes a timestamp if not set.
*
* @param \Drupal\project_browser\ProjectBrowser\Project $project
* The project object containing the ID and source of the project.
* @param string|null $status
* The installation status to set for the project, or NULL if no status.
* The status can be any arbitrary string, depending on the context
* or use case.
*/
public function setState(Project $project, ?string $status): void {
$this->keyValue->setIfNotExists('__timestamp', $this->time->getRequestTime());
if (is_string($status)) {
$this->keyValue->set($project->id, ['source' => $project->source, 'status' => $status]);
}
else {
$this->keyValue->delete($project->id);
}
}
/**
* Retrieves the install state of a project.
*
* @param \Drupal\project_browser\ProjectBrowser\Project $project
* The project object for which to retrieve the install state.
*
* @return string|null
* The current install status of the project, or NULL if not found.
*/
public function getStatus(Project $project): ?string {
$project_data = $this->keyValue->get($project->id);
return $project_data['status'] ?? NULL;
}
/**
* Deletes all project state data from key store.
*/
public function deleteAll(): void {
$this->keyValue->deleteAll();
}
/**
* Retrieves the first updated time of the project states.
*
* @return int|null
* The timestamp when the project states were first updated, or NULL.
*/
public function getFirstUpdatedTime(): ?int {
return $this->keyValue->get('__timestamp');
}
}
...@@ -19,6 +19,7 @@ use Drupal\project_browser\ComposerInstaller\Installer; ...@@ -19,6 +19,7 @@ use Drupal\project_browser\ComposerInstaller\Installer;
use Drupal\project_browser\ComposerInstaller\Validator\CoreNotUpdatedValidator; use Drupal\project_browser\ComposerInstaller\Validator\CoreNotUpdatedValidator;
use Drupal\project_browser\ComposerInstaller\Validator\PackageNotInstalledValidator; use Drupal\project_browser\ComposerInstaller\Validator\PackageNotInstalledValidator;
use Symfony\Component\DependencyInjection\Parameter; use Symfony\Component\DependencyInjection\Parameter;
use Symfony\Component\DependencyInjection\Reference;
/** /**
* Base class acts as a helper for Project Browser services. * Base class acts as a helper for Project Browser services.
...@@ -35,6 +36,10 @@ class ProjectBrowserServiceProvider extends ServiceProviderBase { ...@@ -35,6 +36,10 @@ class ProjectBrowserServiceProvider extends ServiceProviderBase {
$container->register(Installer::class, Installer::class) $container->register(Installer::class, Installer::class)
->setAutowired(TRUE); ->setAutowired(TRUE);
$container->register(InstallState::class, InstallState::class)
->setArgument('$keyValueFactory', new Reference('keyvalue'))
->setAutowired(TRUE);
$container->register(InstallReadiness::class, InstallReadiness::class) $container->register(InstallReadiness::class, InstallReadiness::class)
->setAutowired(TRUE); ->setAutowired(TRUE);
......
...@@ -17,6 +17,7 @@ use Drupal\package_manager\ValidationResult; ...@@ -17,6 +17,7 @@ use Drupal\package_manager\ValidationResult;
use Drupal\package_manager_test_validation\EventSubscriber\TestSubscriber; use Drupal\package_manager_test_validation\EventSubscriber\TestSubscriber;
use Drupal\project_browser\ComposerInstaller\Installer; use Drupal\project_browser\ComposerInstaller\Installer;
use Drupal\project_browser\EnabledSourceHandler; use Drupal\project_browser\EnabledSourceHandler;
use Drupal\project_browser\InstallState;
use Drupal\project_browser_test\Datetime\TestTime; use Drupal\project_browser_test\Datetime\TestTime;
use GuzzleHttp\RequestOptions; use GuzzleHttp\RequestOptions;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
...@@ -35,13 +36,6 @@ class InstallerControllerTest extends BrowserTestBase { ...@@ -35,13 +36,6 @@ class InstallerControllerTest extends BrowserTestBase {
use PackageManagerFixtureUtilityTrait; use PackageManagerFixtureUtilityTrait;
use ApiRequestTrait; use ApiRequestTrait;
/**
* The shared tempstore object.
*
* @var \Drupal\Core\TempStore\SharedTempStore
*/
protected $sharedTempStore;
/** /**
* A stage id. * A stage id.
* *
...@@ -168,7 +162,6 @@ class InstallerControllerTest extends BrowserTestBase { ...@@ -168,7 +162,6 @@ class InstallerControllerTest extends BrowserTestBase {
]); ]);
$query->execute(); $query->execute();
$this->initPackageManager(); $this->initPackageManager();
$this->sharedTempStore = $this->container->get('tempstore.shared');
$this->installer = $this->container->get(Installer::class); $this->installer = $this->container->get(Installer::class);
$this->drupalLogin($this->drupalCreateUser(['administer modules'])); $this->drupalLogin($this->drupalCreateUser(['administer modules']));
$this->config('project_browser.admin_settings') $this->config('project_browser.admin_settings')
...@@ -198,7 +191,7 @@ class InstallerControllerTest extends BrowserTestBase { ...@@ -198,7 +191,7 @@ class InstallerControllerTest extends BrowserTestBase {
* @covers ::begin * @covers ::begin
*/ */
public function testInstallSecurityRevokedModule() { public function testInstallSecurityRevokedModule() {
$this->assertProjectBrowserTempStatus(NULL, NULL); $this->assertSame([], $this->container->get(InstallState::class)->toArray());
$contents = $this->drupalGet('admin/modules/project_browser/install-begin'); $contents = $this->drupalGet('admin/modules/project_browser/install-begin');
$this->stageId = Json::decode($contents)['stage_id']; $this->stageId = Json::decode($contents)['stage_id'];
$response = $this->getPostResponse('project_browser.stage.require', 'project_browser_test_mock/security_revoked_module', [ $response = $this->getPostResponse('project_browser.stage.require', 'project_browser_test_mock/security_revoked_module', [
...@@ -214,7 +207,7 @@ class InstallerControllerTest extends BrowserTestBase { ...@@ -214,7 +207,7 @@ class InstallerControllerTest extends BrowserTestBase {
* @covers ::require * @covers ::require
*/ */
public function testInstallAlreadyPresentPackage() { public function testInstallAlreadyPresentPackage() {
$this->assertProjectBrowserTempStatus(NULL, NULL); $this->assertSame([], $this->container->get(InstallState::class)->toArray());
// Though core is not available as a choice in project browser, it works // 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 // well for the purposes of this test as it's definitely already added
// via composer. // via composer.
...@@ -233,7 +226,7 @@ class InstallerControllerTest extends BrowserTestBase { ...@@ -233,7 +226,7 @@ class InstallerControllerTest extends BrowserTestBase {
* @covers ::begin * @covers ::begin
*/ */
private function doStart() { private function doStart() {
$this->assertProjectBrowserTempStatus(NULL, NULL); $this->assertSame([], $this->container->get(InstallState::class)->toArray());
$contents = $this->drupalGet('admin/modules/project_browser/install-begin'); $contents = $this->drupalGet('admin/modules/project_browser/install-begin');
$this->stageId = Json::decode($contents)['stage_id']; $this->stageId = Json::decode($contents)['stage_id'];
$this->assertSession()->statusCodeEquals(200); $this->assertSession()->statusCodeEquals(200);
...@@ -251,7 +244,7 @@ class InstallerControllerTest extends BrowserTestBase { ...@@ -251,7 +244,7 @@ class InstallerControllerTest extends BrowserTestBase {
]); ]);
$expected_output = sprintf('{"phase":"create","status":0,"stage_id":"%s"}', $this->stageId); $expected_output = sprintf('{"phase":"create","status":0,"stage_id":"%s"}', $this->stageId);
$this->assertSame($expected_output, $this->getSession()->getPage()->getContent()); $this->assertSame($expected_output, $this->getSession()->getPage()->getContent());
$this->assertInstallInProgress('project_browser_test_mock/awesome_module', 'requiring module'); $this->assertInstallInProgress('project_browser_test_mock/awesome_module', 'project_browser_test_mock', 'requiring');
} }
/** /**
...@@ -263,7 +256,7 @@ class InstallerControllerTest extends BrowserTestBase { ...@@ -263,7 +256,7 @@ class InstallerControllerTest extends BrowserTestBase {
$this->drupalGet("/admin/modules/project_browser/install-apply/$this->stageId"); $this->drupalGet("/admin/modules/project_browser/install-apply/$this->stageId");
$expected_output = sprintf('{"phase":"apply","status":0,"stage_id":"%s"}', $this->stageId); $expected_output = sprintf('{"phase":"apply","status":0,"stage_id":"%s"}', $this->stageId);
$this->assertSame($expected_output, $this->getSession()->getPage()->getContent()); $this->assertSame($expected_output, $this->getSession()->getPage()->getContent());
$this->assertInstallInProgress('project_browser_test_mock/awesome_module', 'applying'); $this->assertInstallInProgress('project_browser_test_mock/awesome_module', 'project_browser_test_mock', 'applying');
} }
/** /**
...@@ -275,7 +268,7 @@ class InstallerControllerTest extends BrowserTestBase { ...@@ -275,7 +268,7 @@ class InstallerControllerTest extends BrowserTestBase {
$this->drupalGet("/admin/modules/project_browser/install-post_apply/$this->stageId"); $this->drupalGet("/admin/modules/project_browser/install-post_apply/$this->stageId");
$expected_output = sprintf('{"phase":"post apply","status":0,"stage_id":"%s"}', $this->stageId); $expected_output = sprintf('{"phase":"post apply","status":0,"stage_id":"%s"}', $this->stageId);
$this->assertSame($expected_output, $this->getSession()->getPage()->getContent()); $this->assertSame($expected_output, $this->getSession()->getPage()->getContent());
$this->assertInstallInProgress('project_browser_test_mock/awesome_module', 'post apply'); $this->assertInstallInProgress('project_browser_test_mock/awesome_module', 'project_browser_test_mock', 'applying');
} }
/** /**
...@@ -287,7 +280,7 @@ class InstallerControllerTest extends BrowserTestBase { ...@@ -287,7 +280,7 @@ class InstallerControllerTest extends BrowserTestBase {
$this->drupalGet("/admin/modules/project_browser/install-destroy/$this->stageId"); $this->drupalGet("/admin/modules/project_browser/install-destroy/$this->stageId");
$expected_output = sprintf('{"phase":"destroy","status":0,"stage_id":"%s"}', $this->stageId); $expected_output = sprintf('{"phase":"destroy","status":0,"stage_id":"%s"}', $this->stageId);
$this->assertSame($expected_output, $this->getSession()->getPage()->getContent()); $this->assertSame($expected_output, $this->getSession()->getPage()->getContent());
$this->assertInstallNotInProgress('awesome_module'); $this->assertInstallInProgress('project_browser_test_mock/awesome_module', 'project_browser_test_mock', 'applying');
} }
/** /**
...@@ -450,7 +443,13 @@ class InstallerControllerTest extends BrowserTestBase { ...@@ -450,7 +443,13 @@ class InstallerControllerTest extends BrowserTestBase {
$this->drupalGet('admin/modules/project_browser/install-begin'); $this->drupalGet('admin/modules/project_browser/install-begin');
$this->assertSession()->statusCodeEquals(418); $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->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('project_browser_test_mock/awesome_module', 'requiring module'); $expected = [
'project_browser_test_mock/awesome_module' => [
'source' => 'project_browser_test_mock',
'status' => 'requiring',
],
];
$this->assertSame($expected, $this->container->get(InstallState::class)->toArray());
$this->assertFalse($this->installer->isAvailable()); $this->assertFalse($this->installer->isAvailable());
$this->assertFalse($this->installer->isApplying()); $this->assertFalse($this->installer->isApplying());
TestTime::setFakeTimeByOffset("+800 seconds"); TestTime::setFakeTimeByOffset("+800 seconds");
...@@ -511,7 +510,7 @@ class InstallerControllerTest extends BrowserTestBase { ...@@ -511,7 +510,7 @@ class InstallerControllerTest extends BrowserTestBase {
*/ */
public function testCanBreakStageWithMissingProjectBrowserLock() { public function testCanBreakStageWithMissingProjectBrowserLock() {
$this->doStart(); $this->doStart();
$this->sharedTempStore->get('project_browser')->delete('requiring'); $this->container->get(InstallState::class)->deleteAll();
$content = $this->drupalGet('admin/modules/project_browser/install-begin'); $content = $this->drupalGet('admin/modules/project_browser/install-begin');
$this->assertSession()->statusCodeEquals(418); $this->assertSession()->statusCodeEquals(418);
$this->assertFalse($this->installer->isAvailable()); $this->assertFalse($this->installer->isAvailable());
...@@ -550,67 +549,28 @@ class InstallerControllerTest extends BrowserTestBase { ...@@ -550,67 +549,28 @@ class InstallerControllerTest extends BrowserTestBase {
$assert_session->checkboxChecked('edit-modules-views-ui-enable'); $assert_session->checkboxChecked('edit-modules-views-ui-enable');
} }
/**
* Asserts that a module install is not in progress.
*
* @param string $module
* The module machine name.
*/
protected function assertInstallNotInProgress($module) {
$this->assertProjectBrowserTempStatus(NULL, NULL);
$this->drupalGet("/admin/modules/project_browser/install_in_progress/project_browser_test_mock/$module");
$this->assertSame('{"status":0}', $this->getSession()->getPage()->getContent());
$this->drupalGet('/admin/modules/project_browser/install_in_progress/project_browser_test_mock/metatag');
$this->assertSame('{"status":0}', $this->getSession()->getPage()->getContent());
}
/** /**
* Confirms the project browser in progress input provides the expected value. * Confirms the project browser in progress input provides the expected value.
* *
* @param string $project_id * @param string $project_id
* The ID of the project being enabled. * The ID of the project being enabled.
* @param string $phase * @param string $source
* The install phase. * The project source.
*/ * @param string $status
protected function assertInstallInProgress($project_id, $phase = NULL) { * The install state.
$expect_install = ['project_id' => $project_id]; */
if (!is_null($phase)) { protected function assertInstallInProgress(string $project_id, string $source, ?string $status = NULL) {
$expect_install['phase'] = $phase; $expect_install[$project_id] = [
} 'source' => $source,
$this->assertProjectBrowserTempStatus($expect_install, NULL); 'status' => $status,
];
$this->assertSame($expect_install, $this->container->get(InstallState::class)->toArray());
$this->drupalGet("/admin/modules/project_browser/install_in_progress/$project_id"); $this->drupalGet("/admin/modules/project_browser/install_in_progress/$project_id");
$this->assertSame(sprintf('{"status":1,"phase":"%s"}', $phase), $this->getSession()->getPage()->getContent()); $this->assertSame(sprintf('{"status":1,"phase":"%s"}', $status), $this->getSession()->getPage()->getContent());
$this->drupalGet('/admin/modules/project_browser/install_in_progress/project_browser_test_mock/metatag'); $this->drupalGet('/admin/modules/project_browser/install_in_progress/project_browser_test_mock/metatag');
$this->assertSame('{"status":0}', $this->getSession()->getPage()->getContent()); $this->assertSame('{"status":0}', $this->getSession()->getPage()->getContent());
} }
/**
* Confirms the tempstore install status are as expected.
*
* @param array $expected_requiring
* The expected value of the 'requiring' state.
* @param string $expected_installing
* The expected value of the 'core requiring' state.
*/
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);
}
/** /**
* Sends a POST request to the specified route with the provided project ID. * Sends a POST request to the specified route with the provided project ID.
* *
......
...@@ -10,6 +10,7 @@ use Drupal\Core\State\StateInterface; ...@@ -10,6 +10,7 @@ use Drupal\Core\State\StateInterface;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\project_browser\Traits\PackageManagerFixtureUtilityTrait; use Drupal\Tests\project_browser\Traits\PackageManagerFixtureUtilityTrait;
use Drupal\project_browser\EnabledSourceHandler; use Drupal\project_browser\EnabledSourceHandler;
use Drupal\project_browser\InstallState;
use Drupal\system\SystemManager; use Drupal\system\SystemManager;
/** /**
...@@ -24,11 +25,11 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { ...@@ -24,11 +25,11 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
use ProjectBrowserUiTestTrait, PackageManagerFixtureUtilityTrait; use ProjectBrowserUiTestTrait, PackageManagerFixtureUtilityTrait;
/** /**
* The shared tempstore object. * The install state service.
* *
* @var \Drupal\Core\TempStore\SharedTempStore * @var \Drupal\project_browser\InstallState
*/ */
protected $sharedTempStore; private InstallState $installState;
/** /**
* {@inheritdoc} * {@inheritdoc}
...@@ -54,7 +55,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { ...@@ -54,7 +55,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
$this->initPackageManager(); $this->initPackageManager();
$this->sharedTempStore = $this->container->get('tempstore.shared'); $this->installState = $this->container->get(InstallState::class);
$this->config('project_browser.admin_settings')->set('enabled_sources', ['project_browser_test_mock'])->save(TRUE); $this->config('project_browser.admin_settings')->set('enabled_sources', ['project_browser_test_mock'])->save(TRUE);
$this->config('project_browser.admin_settings')->set('allow_ui_install', TRUE)->save(); $this->config('project_browser.admin_settings')->set('allow_ui_install', TRUE)->save();
...@@ -196,7 +197,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { ...@@ -196,7 +197,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
// Start install begin. // Start install begin.
$this->drupalGet('admin/modules/project_browser/install-begin'); $this->drupalGet('admin/modules/project_browser/install-begin');
$this->sharedTempStore->get('project_browser')->delete('requiring'); $this->installState->deleteAll();
$this->drupalGet('admin/modules/browse'); $this->drupalGet('admin/modules/browse');
$this->svelteInitHelper('text', 'Cream cheese on a bagel'); $this->svelteInitHelper('text', 'Cream cheese on a bagel');
// Try beginning another install while one is in progress, but not yet in // Try beginning another install while one is in progress, but not yet in
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment