Skip to content
Snippets Groups Projects
Commit 67bc9def authored by Ted Bowman's avatar Ted Bowman
Browse files

Issue #3227122 by tedbow, phenaproxima: Create a lock system to ensure only 1...

Issue #3227122 by tedbow, phenaproxima: Create a lock system to ensure only 1 update can be attempted at a time
parent 39ecb2e5
No related branches found
No related tags found
1 merge request!113Issue #3227122: Create a lock system to ensure only 1 update can be attempted at a time
...@@ -183,6 +183,8 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase { ...@@ -183,6 +183,8 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
$assert_session->pageTextNotContains(static::$warningsExplanation); $assert_session->pageTextNotContains(static::$warningsExplanation);
$page->pressButton('Update'); $page->pressButton('Update');
$this->checkForMetaRefresh(); $this->checkForMetaRefresh();
// If a validator flags an error, but doesn't throw, the update should still
// be halted.
$assert_session->pageTextContainsOnce('An error has occurred.'); $assert_session->pageTextContainsOnce('An error has occurred.');
$page->clickLink('the error page'); $page->clickLink('the error page');
$assert_session->pageTextContainsOnce((string) $expected_results[0]->getMessages()[0]); $assert_session->pageTextContainsOnce((string) $expected_results[0]->getMessages()[0]);
...@@ -190,11 +192,10 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase { ...@@ -190,11 +192,10 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
$assert_session->pageTextNotContains($expected_results[0]->getSummary()); $assert_session->pageTextNotContains($expected_results[0]->getSummary());
// ...but we should see the exception message. // ...but we should see the exception message.
$assert_session->pageTextContainsOnce('The update exploded.'); $assert_session->pageTextContainsOnce('The update exploded.');
// If the error is thrown in PreCreateEvent the update stage will not have
// If a validator flags an error, but doesn't throw, the update should still // been created.
// be halted. $assert_session->buttonNotExists('Delete existing update');
TestChecker1::setTestResult($expected_results, PreCreateEvent::class); TestChecker1::setTestResult($expected_results, PreCreateEvent::class);
$this->deleteStagedUpdate();
$page->pressButton('Update'); $page->pressButton('Update');
$this->checkForMetaRefresh(); $this->checkForMetaRefresh();
$assert_session->pageTextContainsOnce('An error has occurred.'); $assert_session->pageTextContainsOnce('An error has occurred.');
...@@ -207,8 +208,6 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase { ...@@ -207,8 +208,6 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
// continue. // continue.
$expected_results = $this->testResults['checker_1']['1 warning']; $expected_results = $this->testResults['checker_1']['1 warning'];
TestChecker1::setTestResult($expected_results, PreCreateEvent::class); TestChecker1::setTestResult($expected_results, PreCreateEvent::class);
$session->reload();
$this->deleteStagedUpdate();
$page->pressButton('Update'); $page->pressButton('Update');
$this->checkForMetaRefresh(); $this->checkForMetaRefresh();
$assert_session->pageTextContains('Ready to update'); $assert_session->pageTextContains('Ready to update');
...@@ -234,13 +233,41 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase { ...@@ -234,13 +233,41 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
} }
/** /**
* Deletes a staged, failed update. * Tests deleting an existing update.
*
* @todo Add test coverage for differences between stage owner and other users
* in https://www.drupal.org/i/3248928.
*/ */
private function deleteStagedUpdate(): void { public function testDeleteExistingUpdate() {
$session = $this->getSession(); $assert_session = $this->assertSession();
$session->getPage()->pressButton('Delete existing update'); $page = $this->getSession()->getPage();
$this->assertSession()->pageTextContains('Staged update deleted'); $this->setCoreVersion('9.8.0');
$session->reload(); $this->checkForUpdates();
$this->drupalGet('/admin/modules/automatic-update');
$page->pressButton('Update');
$this->checkForMetaRefresh();
// Confirm we are on the confirmation page.
$assert_session->addressEquals('/admin/automatic-update-ready');
$assert_session->buttonExists('Continue');
// Return to the start page.
$this->drupalGet('/admin/modules/automatic-update');
$assert_session->pageTextContainsOnce('Cannot begin an update because another Composer operation is currently in progress.');
$assert_session->buttonNotExists('Update');
// Delete the existing update.
$page->pressButton('Delete existing update');
$assert_session->pageTextNotContains('Cannot begin an update because another Composer operation is currently in progress.');
// Ensure we can start another update after deleting the existing one.
$page->pressButton('Update');
$this->checkForMetaRefresh();
// Confirm we are on the confirmation page.
$assert_session->addressEquals('/admin/automatic-update-ready');
$assert_session->buttonExists('Continue');
} }
} }
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
namespace Drupal\Tests\automatic_updates\Kernel\ReadinessValidation; namespace Drupal\Tests\automatic_updates\Kernel\ReadinessValidation;
use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\package_manager\Event\PreApplyEvent;
use Drupal\package_manager\PathLocator; use Drupal\package_manager\PathLocator;
use Drupal\package_manager\StageException; use Drupal\package_manager\StageException;
use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase; use Drupal\Tests\automatic_updates\Kernel\AutomaticUpdatesKernelTestBase;
...@@ -20,6 +21,7 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase { ...@@ -20,6 +21,7 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
protected static $modules = [ protected static $modules = [
'automatic_updates', 'automatic_updates',
'package_manager', 'package_manager',
'package_manager_bypass',
]; ];
/** /**
...@@ -47,15 +49,20 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase { ...@@ -47,15 +49,20 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
private function validate(string $active_dir, string $stage_dir): array { private function validate(string $active_dir, string $stage_dir): array {
$locator = $this->prophesize(PathLocator::class); $locator = $this->prophesize(PathLocator::class);
$locator->getActiveDirectory()->willReturn($active_dir); $locator->getActiveDirectory()->willReturn($active_dir);
$locator->getProjectRoot()->willReturn($active_dir);
$locator->getVendorDirectory()->willReturn($active_dir);
$locator->getStageDirectory()->willReturn($stage_dir); $locator->getStageDirectory()->willReturn($stage_dir);
$this->container->set('package_manager.path_locator', $locator->reveal()); $this->container->set('package_manager.path_locator', $locator->reveal());
$updater = $this->container->get('automatic_updates.updater');
$updater->begin(['drupal' => '9.8.1']);
// The staged projects validator only runs before staged updates are // The staged projects validator only runs before staged updates are
// applied. Since the active and stage directories may not exist, we don't // applied. Since the active and stage directories may not exist, we don't
// want to invoke the other stages of the update because they may raise // want to invoke the other stages of the update because they may raise
// errors that are outside of the scope of what we're testing here. // errors that are outside of the scope of what we're testing here.
try { try {
$this->container->get('automatic_updates.updater')->apply(); $updater->apply();
return []; return [];
} }
catch (StageException $e) { catch (StageException $e) {
...@@ -67,11 +74,29 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase { ...@@ -67,11 +74,29 @@ class StagedProjectsValidatorTest extends AutomaticUpdatesKernelTestBase {
* Tests that if an exception is thrown, the event will absorb it. * Tests that if an exception is thrown, the event will absorb it.
*/ */
public function testEventConsumesExceptionResults(): void { public function testEventConsumesExceptionResults(): void {
$results = $this->validate('/fake/active/dir', '/fake/stage/dir'); // Prepare a fake site in the virtual file system which contains valid
// Composer data.
$fixture = __DIR__ . '/../../../fixtures/fake-site';
copy("$fixture/composer.json", 'public://composer.json');
copy("$fixture/composer.lock", 'public://composer.lock');
$event_dispatcher = $this->container->get('event_dispatcher');
// Disable the disk space validator, since it doesn't work with vfsStream.
$disk_space_validator = $this->container->get('package_manager.validator.disk_space');
$event_dispatcher->removeSubscriber($disk_space_validator);
// Just before the staged changes are applied, delete the composer.json file
// to trigger an error. This uses the highest possible priority to guarantee
// it runs before any other subscribers.
$listener = function () {
unlink('public://composer.json');
};
$event_dispatcher->addListener(PreApplyEvent::class, $listener, PHP_INT_MAX);
$results = $this->validate('public://', '/fake/stage/dir');
$this->assertCount(1, $results); $this->assertCount(1, $results);
$messages = reset($results)->getMessages(); $messages = reset($results)->getMessages();
$this->assertCount(1, $messages); $this->assertCount(1, $messages);
$this->assertStringContainsString('Composer could not find the config file: /fake/active/dir/composer.json', (string) reset($messages)); $this->assertStringContainsString('Composer could not find the config file: public:///composer.json', (string) reset($messages));
} }
/** /**
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
namespace Drupal\Tests\automatic_updates\Kernel; namespace Drupal\Tests\automatic_updates\Kernel;
use Drupal\package_manager\PathLocator; use Drupal\package_manager\PathLocator;
use Drupal\Tests\user\Traits\UserCreationTrait;
use PhpTuf\ComposerStager\Domain\StagerInterface; use PhpTuf\ComposerStager\Domain\StagerInterface;
use Prophecy\Argument; use Prophecy\Argument;
...@@ -13,6 +14,8 @@ use Prophecy\Argument; ...@@ -13,6 +14,8 @@ use Prophecy\Argument;
*/ */
class UpdaterTest extends AutomaticUpdatesKernelTestBase { class UpdaterTest extends AutomaticUpdatesKernelTestBase {
use UserCreationTrait;
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
...@@ -21,14 +24,28 @@ class UpdaterTest extends AutomaticUpdatesKernelTestBase { ...@@ -21,14 +24,28 @@ class UpdaterTest extends AutomaticUpdatesKernelTestBase {
'automatic_updates_test', 'automatic_updates_test',
'package_manager', 'package_manager',
'package_manager_bypass', 'package_manager_bypass',
'system',
'user',
]; ];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
}
/** /**
* Tests that correct versions are staged after calling ::begin(). * Tests that correct versions are staged after calling ::begin().
*/ */
public function testCorrectVersionsStaged() { public function testCorrectVersionsStaged() {
$this->setReleaseMetadata(__DIR__ . '/../../fixtures/release-history/drupal.9.8.1-security.xml'); $this->setReleaseMetadata(__DIR__ . '/../../fixtures/release-history/drupal.9.8.1-security.xml');
// Create a user who will own the stage even after the container is rebuilt.
$user = $this->createUser([], NULL, TRUE, ['uid' => 2]);
$this->setCurrentUser($user);
// Point to a fake site which requires Drupal core via a distribution. The // Point to a fake site which requires Drupal core via a distribution. The
// lock file should be scanned to determine the core packages, which should // lock file should be scanned to determine the core packages, which should
// result in drupal/core-recommended being updated. // result in drupal/core-recommended being updated.
...@@ -48,8 +65,9 @@ class UpdaterTest extends AutomaticUpdatesKernelTestBase { ...@@ -48,8 +65,9 @@ class UpdaterTest extends AutomaticUpdatesKernelTestBase {
$kernel = $this->container->get('kernel'); $kernel = $this->container->get('kernel');
$kernel->rebuildContainer(); $kernel->rebuildContainer();
$this->container = $kernel->getContainer(); $this->container = $kernel->getContainer();
// Keep using the mocked path locator. // Keep using the mocked path locator and current user.
$this->container->set('package_manager.path_locator', $locator->reveal()); $this->container->set('package_manager.path_locator', $locator->reveal());
$this->setCurrentUser($user);
// When we call Updater::stage(), the stored project versions should be // When we call Updater::stage(), the stored project versions should be
// read from state and passed to Composer Stager's Stager service, in the // read from state and passed to Composer Stager's Stager service, in the
......
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