Skip to content
Snippets Groups Projects
Commit fcbb50e6 authored by Adam G-H's avatar Adam G-H
Browse files

Issue #3230024 by phenaproxima, tedbow: Collect exclusions from event objects

parent 648f8901
No related branches found
No related tags found
No related merge requests found
Showing
with 406 additions and 288 deletions
......@@ -4,7 +4,7 @@ services:
arguments: ['@keyvalue.expirable', '@datetime.time', 24]
automatic_updates.updater:
class: Drupal\automatic_updates\Updater
arguments: ['@state', '@string_translation','@automatic_updates.beginner', '@automatic_updates.stager', '@automatic_updates.cleaner', '@automatic_updates.committer' , '@file_system', '@event_dispatcher', '%app.root%', '%site.path%']
arguments: ['@state', '@string_translation','@automatic_updates.beginner', '@automatic_updates.stager', '@automatic_updates.cleaner', '@automatic_updates.committer', '@event_dispatcher']
automatic_updates.staged_package_validator:
class: Drupal\automatic_updates\Validation\StagedProjectsValidation
arguments: ['@string_translation', '@automatic_updates.updater' ]
......@@ -70,3 +70,8 @@ services:
['@automatic_updates.symfony_exec_finder' ]
automatic_updates.process_factory:
class: Drupal\automatic_updates\ComposerStager\ProcessFactory
automatic_updates.excluded_paths_subscriber:
class: Drupal\automatic_updates\Event\ExcludedPathsSubscriber
arguments: ['%app.root%', '%site.path%', '@file_system']
tags:
- { name: event_subscriber }
<?php
namespace Drupal\automatic_updates\Event;
use Drupal\automatic_updates\AutomaticUpdatesEvents;
use Drupal\Core\File\FileSystemInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Defines an event subscriber to exclude certain paths from update operations.
*/
class ExcludedPathsSubscriber implements EventSubscriberInterface {
/**
* The Drupal root.
*
* @var string
*/
protected $appRoot;
/**
* The current site path, relative to the Drupal root.
*
* @var string
*/
protected $sitePath;
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* Constructs an UpdateSubscriber.
*
* @param string $app_root
* The Drupal root.
* @param string $site_path
* The current site path, relative to the Drupal root.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system service.
*/
public function __construct(string $app_root, string $site_path, FileSystemInterface $file_system) {
$this->appRoot = $app_root;
$this->sitePath = $site_path;
$this->fileSystem = $file_system;
}
/**
* Reacts to the beginning of an update process.
*
* @param \Drupal\automatic_updates\Event\PreStartEvent $event
* The event object.
*/
public function preStart(PreStartEvent $event): void {
if ($public = $this->fileSystem->realpath('public://')) {
$event->excludePath($public);
}
if ($private = $this->fileSystem->realpath('private://')) {
$event->excludePath($private);
}
// If this module is a git clone, exclude it.
if (is_dir(__DIR__ . '/../../.git')) {
$event->excludePath($this->fileSystem->realpath(__DIR__ . '/../..'));
}
// Exclude site-specific settings files.
$settings_files = [
'settings.php',
'settings.local.php',
'services.yml',
];
foreach ($settings_files as $settings_file) {
$file_path = implode(DIRECTORY_SEPARATOR, [
$this->appRoot,
$this->sitePath,
$settings_file,
]);
$file_path = $this->fileSystem->realpath($file_path);
if (file_exists($file_path)) {
$event->excludePath($file_path);
}
$default_file_path = implode(DIRECTORY_SEPARATOR, [
'sites',
'default',
$settings_file,
]);
$event->excludePath($default_file_path);
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
return [
AutomaticUpdatesEvents::PRE_START => 'preStart',
];
}
}
<?php
namespace Drupal\automatic_updates\Event;
/**
* Common functionality for events which can collect excluded paths.
*/
trait ExcludedPathsTrait {
/**
* Paths to exclude from the update.
*
* @var string[]
*/
protected $excludedPaths = [];
/**
* Adds an absolute path to exclude from the update operation.
*
* @todo This should only accept paths relative to the active directory.
*
* @param string $path
* The path to exclude.
*/
public function excludePath(string $path): void {
$this->excludedPaths[] = $path;
}
/**
* Returns the paths to exclude from the update operation.
*
* @return string[]
* The paths to exclude.
*/
public function getExcludedPaths(): array {
return array_unique($this->excludedPaths);
}
}
<?php
namespace Drupal\automatic_updates\Event;
/**
* Event fired before staged changes are copied into the active site.
*/
class PreCommitEvent extends UpdateEvent {
use ExcludedPathsTrait;
}
<?php
namespace Drupal\automatic_updates\Event;
/**
* Event fired before an update begins.
*/
class PreStartEvent extends UpdateEvent {
use ExcludedPathsTrait;
}
<?php
namespace Drupal\automatic_updates\Form;
use Drupal\automatic_updates\Exception\UpdateException;
use Drupal\automatic_updates\Updater;
use Drupal\automatic_updates\Validation\ValidationResult;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Defines a base class for forms which are part of the attended update process.
*/
abstract class UpdateFormBase extends FormBase {
/**
* The updater service.
*
* @var \Drupal\automatic_updates\Updater
*/
protected $updater;
/**
* Constructs an UpdateFormBase object.
*
* @param \Drupal\automatic_updates\Updater $updater
* The updater service.
*/
public function __construct(Updater $updater) {
$this->updater = $updater;
}
/**
* Fires an update validation event and handles any detected errors.
*
* If $form and $form_state are passed, errors will be flagged against the
* form_id element, since it's guaranteed to exist in all forms. Otherwise,
* the errors will be displayed in the messages area.
*
* @param string $event
* The name of the event to fire. Should be one of the constants from
* \Drupal\automatic_updates\AutomaticUpdatesEvents.
* @param array|null $form
* (optional) The complete form array.
* @param \Drupal\Core\Form\FormStateInterface|null $form_state
* (optional) The current form state.
*
* @return bool
* TRUE if no errors were found, FALSE otherwise.
*/
protected function validateUpdate(string $event, array &$form = NULL, FormStateInterface $form_state = NULL): bool {
$errors = FALSE;
foreach ($this->getValidationErrors($event) as $error) {
if ($form && $form_state) {
$form_state->setError($form['form_id'], $error);
}
else {
$this->messenger()->addError($error);
}
$errors = TRUE;
}
return !$errors;
}
/**
* Fires an update validation event and returns all resulting errors.
*
* @param string $event
* The name of the event to fire. Should be one of the constants from
* \Drupal\automatic_updates\AutomaticUpdatesEvents.
*
* @return \Drupal\Component\Render\MarkupInterface[]
* The validation errors, if any.
*/
protected function getValidationErrors(string $event): array {
$errors = [];
try {
$this->updater->dispatchUpdateEvent($event);
}
catch (UpdateException $e) {
foreach ($e->getValidationResults() as $result) {
$errors = array_merge($errors, $this->getMessagesFromValidationResult($result));
}
}
return $errors;
}
/**
* Extracts all relevant messages from an update validation result.
*
* @param \Drupal\automatic_updates\Validation\ValidationResult $result
* The validation result.
*
* @return \Drupal\Component\Render\MarkupInterface[]
* The messages to display from the validation result.
*/
protected function getMessagesFromValidationResult(ValidationResult $result): array {
$messages = $result->getMessages();
if (count($messages) > 1) {
array_unshift($messages, $result->getSummary());
}
return $messages;
}
}
......@@ -2,10 +2,10 @@
namespace Drupal\automatic_updates\Form;
use Drupal\automatic_updates\AutomaticUpdatesEvents;
use Drupal\automatic_updates\BatchProcessor;
use Drupal\automatic_updates\Updater;
use Drupal\Core\Batch\BatchBuilder;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\State\StateInterface;
......@@ -17,7 +17,14 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
* @internal
* Form classes are internal.
*/
class UpdateReady extends UpdateFormBase {
class UpdateReady extends FormBase {
/**
* The updater service.
*
* @var \Drupal\automatic_updates\Updater
*/
protected $updater;
/**
* The state service.
......@@ -37,7 +44,7 @@ class UpdateReady extends UpdateFormBase {
* The state service.
*/
public function __construct(Updater $updater, MessengerInterface $messenger, StateInterface $state) {
parent::__construct($updater);
$this->updater = $updater;
$this->setMessenger($messenger);
$this->state = $state;
}
......@@ -64,10 +71,6 @@ class UpdateReady extends UpdateFormBase {
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
if (!$this->validateUpdate(AutomaticUpdatesEvents::PRE_COMMIT)) {
return $form;
}
$form['backup'] = [
'#prefix' => '<strong>',
'#markup' => $this->t('Back up your database and site before you continue. <a href=":backup_url">Learn how</a>.', [':backup_url' => 'https://www.drupal.org/node/22281']),
......@@ -89,14 +92,6 @@ class UpdateReady extends UpdateFormBase {
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
$this->validateUpdate(AutomaticUpdatesEvents::PRE_COMMIT, $form, $form_state);
}
/**
* {@inheritdoc}
*/
......
......@@ -2,12 +2,12 @@
namespace Drupal\automatic_updates\Form;
use Drupal\automatic_updates\AutomaticUpdatesEvents;
use Drupal\automatic_updates\BatchProcessor;
use Drupal\automatic_updates\Updater;
use Drupal\automatic_updates_9_3_shim\ProjectRelease;
use Drupal\Core\Batch\BatchBuilder;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\State\StateInterface;
......@@ -21,7 +21,14 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
* @internal
* Form classes are internal.
*/
class UpdaterForm extends UpdateFormBase {
class UpdaterForm extends FormBase {
/**
* The updater service.
*
* @var \Drupal\automatic_updates\Updater
*/
protected $updater;
/**
* The module handler.
......@@ -38,7 +45,7 @@ class UpdaterForm extends UpdateFormBase {
protected $state;
/**
* Constructs a new UpdateManagerUpdate object.
* Constructs a new UpdaterForm object.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
......@@ -48,7 +55,7 @@ class UpdaterForm extends UpdateFormBase {
* The updater service.
*/
public function __construct(ModuleHandlerInterface $module_handler, StateInterface $state, Updater $updater) {
parent::__construct($updater);
$this->updater = $updater;
$this->moduleHandler = $module_handler;
$this->state = $state;
}
......@@ -213,14 +220,6 @@ class UpdaterForm extends UpdateFormBase {
return $actions;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
$this->validateUpdate(AutomaticUpdatesEvents::PRE_START, $form, $form_state);
}
/**
* Submit function to delete an existing in-progress update.
*/
......
......@@ -3,9 +3,10 @@
namespace Drupal\automatic_updates;
use Composer\Autoload\ClassLoader;
use Drupal\automatic_updates\Event\PreCommitEvent;
use Drupal\automatic_updates\Event\PreStartEvent;
use Drupal\automatic_updates\Event\UpdateEvent;
use Drupal\automatic_updates\Exception\UpdateException;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\State\StateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
......@@ -23,6 +24,16 @@ class Updater {
use StringTranslationTrait;
/**
* The event classes to dispatch for various update events.
*
* @var string[]
*/
protected const EVENT_CLASSES = [
AutomaticUpdatesEvents::PRE_START => PreStartEvent::class,
AutomaticUpdatesEvents::PRE_COMMIT => PreCommitEvent::class,
];
/**
* The state key in which to store the status of the update.
*
......@@ -65,13 +76,6 @@ class Updater {
*/
protected $state;
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* The event dispatcher service.
*
......@@ -79,20 +83,6 @@ class Updater {
*/
protected $eventDispatcher;
/**
* The Drupal root.
*
* @var string
*/
protected $appRoot;
/**
* The current site directory, relative to the Drupal root.
*
* @var string
*/
protected $sitePath;
/**
* Constructs an Updater object.
*
......@@ -108,26 +98,17 @@ class Updater {
* The Composer Stager's cleaner service.
* @param \PhpTuf\ComposerStager\Domain\CommitterInterface $committer
* The Composer Stager's committer service.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system service.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher service.
* @param string $app_root
* The Drupal root.
* @param string $site_path
* The current site directory, relative to the Drupal root.
*/
public function __construct(StateInterface $state, TranslationInterface $translation, BeginnerInterface $beginner, StagerInterface $stager, CleanerInterface $cleaner, CommitterInterface $committer, FileSystemInterface $file_system, EventDispatcherInterface $event_dispatcher, string $app_root, string $site_path) {
public function __construct(StateInterface $state, TranslationInterface $translation, BeginnerInterface $beginner, StagerInterface $stager, CleanerInterface $cleaner, CommitterInterface $committer, EventDispatcherInterface $event_dispatcher) {
$this->state = $state;
$this->beginner = $beginner;
$this->stager = $stager;
$this->cleaner = $cleaner;
$this->committer = $committer;
$this->setStringTranslation($translation);
$this->fileSystem = $file_system;
$this->eventDispatcher = $event_dispatcher;
$this->appRoot = $app_root;
$this->sitePath = $site_path;
}
/**
......@@ -183,51 +164,25 @@ class Updater {
*/
public function begin(): string {
$stage_key = $this->createActiveStage();
$this->beginner->begin(static::getActiveDirectory(), static::getStageDirectory(), NULL, 120, $this->getExclusions());
$event = $this->dispatchUpdateEvent(AutomaticUpdatesEvents::PRE_START);
$this->beginner->begin(static::getActiveDirectory(), static::getStageDirectory(), NULL, 120, $this->getExclusions($event));
return $stage_key;
}
/**
* Gets the paths that should be excluded from the staging area.
* Gets the excluded paths collected by an event object.
*
* @param \Drupal\automatic_updates\Event\PreStartEvent|\Drupal\automatic_updates\Event\PreCommitEvent $event
* The event object.
*
* @return string[]
* The paths to exclude, relative to the active directory.
*/
protected function getExclusions(): array {
$exclusions = [];
if ($public = $this->fileSystem->realpath('public://')) {
$exclusions[] = $public;
}
if ($private = $this->fileSystem->realpath('private://')) {
$exclusions[] = $private;
}
// If this module is a git clone, exclude it.
if (is_dir(__DIR__ . '/../.git')) {
$exclusions[] = $this->fileSystem->realpath(__DIR__ . '/..');
}
// Exclude site-specific settings files.
$settings_files = [
'settings.php',
'settings.local.php',
'services.yml',
];
foreach ($settings_files as $settings_file) {
$file_path = implode(DIRECTORY_SEPARATOR, [
$this->appRoot,
$this->sitePath,
$settings_file,
]);
$file_path = $this->fileSystem->realpath($file_path);
if (file_exists($file_path)) {
$exclusions[] = $file_path;
}
}
private function getExclusions($event): array {
$make_relative = function (string $path): string {
return str_replace($this->getActiveDirectory() . '/', '', $path);
};
return array_map($make_relative, $exclusions);
return array_map($make_relative, $event->getExcludedPaths());
}
/**
......@@ -272,6 +227,10 @@ class Updater {
* Commits the current update.
*/
public function commit(): void {
$this->dispatchUpdateEvent(AutomaticUpdatesEvents::PRE_COMMIT);
// @todo Pass excluded paths into the committer once
// https://github.com/php-tuf/composer-stager/pull/14 is in a tagged release
// of Composer Stager.
$this->committer->commit($this->getStageDirectory(), static::getActiveDirectory());
}
......@@ -316,16 +275,21 @@ class Updater {
* @param string $event_name
* The name of the event to dispatch.
*
* @return \Drupal\automatic_updates\Event\UpdateEvent
* The event object.
*
* @throws \Drupal\automatic_updates\Exception\UpdateException
* If any of the event subscribers adds a validation error.
*/
public function dispatchUpdateEvent(string $event_name): void {
$event = new UpdateEvent();
public function dispatchUpdateEvent(string $event_name): UpdateEvent {
$class = static::EVENT_CLASSES[$event_name] ?? UpdateEvent::class;
$event = new $class();
$this->eventDispatcher->dispatch($event, $event_name);
if ($checker_results = $event->getResults(SystemManager::REQUIREMENT_ERROR)) {
throw new UpdateException($checker_results,
"Unable to complete the update because of errors.");
}
return $event;
}
}
......@@ -40,13 +40,13 @@ class TestChecker1 implements EventSubscriberInterface {
* This method is static to enable setting the expected messages before the
* test module is enabled.
*
* @param \Drupal\automatic_updates\Validation\ValidationResult[] $checker_results
* The test validation result.
* @param \Drupal\automatic_updates\Validation\ValidationResult[]|\Throwable $checker_results
* The test validation results, or an exception to throw.
* @param string $event_name
* (optional )The event name. Defaults to
* AutomaticUpdatesEvents::READINESS_CHECK.
*/
public static function setTestResult(array $checker_results, string $event_name = AutomaticUpdatesEvents::READINESS_CHECK): void {
public static function setTestResult($checker_results, string $event_name = AutomaticUpdatesEvents::READINESS_CHECK): void {
\Drupal::state()->set(static::STATE_KEY . ".$event_name", $checker_results);
}
......@@ -60,6 +60,9 @@ class TestChecker1 implements EventSubscriberInterface {
*/
protected function addResults(UpdateEvent $event, string $state_key): void {
$results = $this->state->get($state_key, []);
if ($results instanceof \Throwable) {
throw $results;
}
foreach ($results as $result) {
$event->addValidationResult($result);
}
......
<?php
namespace Drupal\automatic_updates_test;
use Drupal\automatic_updates\Exception\UpdateException;
use Drupal\automatic_updates\Updater;
/**
* A test-only updater which can throw errors during the update process.
*/
class TestUpdater extends Updater {
/**
* Sets the errors to be thrown during the begin() method.
*
* @param \Drupal\automatic_updates\Validation\ValidationResult[] $errors
* The validation errors that should be thrown.
*/
public static function setBeginErrors(array $errors): void {
\Drupal::state()->set('automatic_updates_test.updater_errors', [
'begin' => $errors,
]);
}
/**
* {@inheritdoc}
*/
public function begin(): string {
$errors = $this->state->get('automatic_updates_test.updater_errors', []);
if (isset($errors['begin'])) {
throw new UpdateException($errors['begin'], reset($errors['begin'])->getSummary());
}
return parent::begin();
}
}
name: 'Composer Stager bypass'
description: 'Mocks Composer Stager services for functional testing'
type: module
package: Testing
dependencies:
- automatic_updates:automatic_updates
services:
composer_stager_bypass.committer:
public: false
class: Drupal\composer_stager_bypass\Committer
decorates: automatic_updates.committer
arguments:
- '@composer_stager_bypass.committer.inner'
properties:
- { _serviceId: automatic_updates.committer }
<?php
namespace Drupal\composer_stager_bypass;
use PhpTuf\ComposerStager\Domain\BeginnerInterface;
use PhpTuf\ComposerStager\Domain\Output\ProcessOutputCallbackInterface;
/**
* Defines an update beginner which doesn't do anything.
*/
class Beginner implements BeginnerInterface {
/**
* {@inheritdoc}
*/
public function begin(string $activeDir, string $stagingDir, ?ProcessOutputCallbackInterface $callback = NULL, ?int $timeout = 120): void {
}
}
<?php
namespace Drupal\composer_stager_bypass;
use PhpTuf\ComposerStager\Domain\CommitterInterface;
use PhpTuf\ComposerStager\Domain\Output\ProcessOutputCallbackInterface;
/**
* Defines an update committer which doesn't do any actual committing.
*/
class Committer implements CommitterInterface {
/**
* The decorated committer service.
*
* @var \PhpTuf\ComposerStager\Domain\CommitterInterface
*/
private $decorated;
/**
* Constructs a Committer object.
*
* @param \PhpTuf\ComposerStager\Domain\CommitterInterface $decorated
* The decorated committer service.
*/
public function __construct(CommitterInterface $decorated) {
$this->decorated = $decorated;
}
/**
* {@inheritdoc}
*/
public function commit(string $stagingDir, string $activeDir, ?ProcessOutputCallbackInterface $callback = NULL, ?int $timeout = 120): void {
}
/**
* {@inheritdoc}
*/
public function directoryExists(string $stagingDir): bool {
return $this->decorated->directoryExists($stagingDir);
}
}
<?php
namespace Drupal\automatic_updates_test;
namespace Drupal\composer_stager_bypass;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;
/**
* Defines a service provider for testing automatic updates.
* Defines services to bypass Composer Stager's core functionality.
*/
class AutomaticUpdatesTestServiceProvider extends ServiceProviderBase {
class ComposerStagerBypassServiceProvider extends ServiceProviderBase {
/**
* {@inheritdoc}
......@@ -16,14 +16,10 @@ class AutomaticUpdatesTestServiceProvider extends ServiceProviderBase {
public function alter(ContainerBuilder $container) {
parent::alter($container);
$modules = $container->getParameter('container.modules');
if (isset($modules['automatic_updates'])) {
// Swap in our special updater implementation, which can be rigged to
// throw errors during various points in the update process in order to
// test error handling during updates.
$container->getDefinition('automatic_updates.updater')
->setClass(TestUpdater::class);
}
$container->getDefinition('automatic_updates.beginner')
->setClass(Beginner::class);
$container->getDefinition('automatic_updates.stager')
->setClass(Stager::class);
}
}
<?php
namespace Drupal\composer_stager_bypass;
use PhpTuf\ComposerStager\Domain\Output\ProcessOutputCallbackInterface;
use PhpTuf\ComposerStager\Domain\StagerInterface;
/**
* Defines an update stager which doesn't actually do anything.
*/
class Stager implements StagerInterface {
/**
* {@inheritdoc}
*/
public function stage(array $composerCommand, string $stagingDir, ?ProcessOutputCallbackInterface $callback = NULL, ?int $timeout = 120): void {
}
}
......@@ -2,6 +2,7 @@
namespace Drupal\Tests\automatic_updates\Functional;
use Drupal\automatic_updates\Event\PreStartEvent;
use Drupal\Tests\BrowserTestBase;
/**
......@@ -51,13 +52,16 @@ class ExclusionsTest extends BrowserTestBase {
* @covers \Drupal\automatic_updates\Updater::getExclusions
*/
public function testExclusions(): void {
$event = new PreStartEvent();
$this->container->get('automatic_updates.excluded_paths_subscriber')
->preStart($event);
/** @var \Drupal\automatic_updates\Updater $updater */
$updater = $this->container->get('automatic_updates.updater');
$reflector = new \ReflectionObject($updater);
$method = $reflector->getMethod('getExclusions');
$method->setAccessible(TRUE);
$exclusions = $method->invoke($updater);
$exclusions = $method->invoke($updater, $event);
$this->assertContains("$this->siteDirectory/files", $exclusions);
$this->assertContains("$this->siteDirectory/private", $exclusions);
......
......@@ -3,9 +3,8 @@
namespace Drupal\Tests\automatic_updates\Functional;
use Drupal\automatic_updates\AutomaticUpdatesEvents;
use Drupal\automatic_updates\Validation\ValidationResult;
use Drupal\automatic_updates\Exception\UpdateException;
use Drupal\automatic_updates_test\ReadinessChecker\TestChecker1;
use Drupal\automatic_updates_test\TestUpdater;
use Drupal\Tests\automatic_updates\Traits\ValidationTestTrait;
use Drupal\Tests\BrowserTestBase;
......@@ -29,6 +28,7 @@ class UpdaterFormTest extends BrowserTestBase {
protected static $modules = [
'automatic_updates',
'automatic_updates_test',
'composer_stager_bypass',
'update_test',
];
......@@ -102,65 +102,66 @@ class UpdaterFormTest extends BrowserTestBase {
}
/**
* Tests that the form runs update validators before starting the batch job.
* Tests handling of errors and warnings during the update process.
*/
public function testValidation(): void {
$this->setCoreVersion('9.8.0');
public function testUpdateErrors(): void {
$session = $this->getSession();
$assert_session = $this->assertSession();
$page = $session->getPage();
// Ensure that one of the update validators will produce an error when we
// try to run updates.
$this->setCoreVersion('9.8.0');
$this->createTestValidationResults();
$expected_results = $this->testResults['checker_1']['1 error'];
TestChecker1::setTestResult($expected_results, AutomaticUpdatesEvents::PRE_START);
// Repackage the validation error as an exception, so we can test what
// happens if a validator throws.
$error = new UpdateException($expected_results, 'The update exploded.');
TestChecker1::setTestResult($error, AutomaticUpdatesEvents::PRE_START);
$this->drupalLogin($this->rootUser);
$this->checkForUpdates();
$this->drupalGet('/admin/automatic-update');
$this->getSession()->getPage()->pressButton('Download these updates');
$page->pressButton('Download these updates');
$this->checkForMetaRefresh();
$assert_session->pageTextContains('An error has occurred.');
$page->clickLink('the error page');
$assert_session->pageTextContains((string) $expected_results[0]->getMessages()[0]);
// Since there's only one error message, we shouldn't see the summary...
$assert_session->pageTextNotContains($expected_results[0]->getSummary());
// ...but we should see the exception message.
$assert_session->pageTextContains('The update exploded.');
$assert_session = $this->assertSession();
// We should still be on the same page, having not passed validation.
$assert_session->addressEquals('/admin/automatic-update');
foreach ($expected_results[0]->getMessages() as $message) {
$assert_session->pageTextContains($message);
}
// Since there is only one error message, we shouldn't see the summary.
// If a validator flags an error, but doesn't throw, the update should still
// be halted.
TestChecker1::setTestResult($expected_results, AutomaticUpdatesEvents::PRE_START);
$this->deleteStagedUpdate();
$page->pressButton('Download these updates');
$this->checkForMetaRefresh();
$assert_session->pageTextContains('An error has occurred.');
$page->clickLink('the error page');
// Since there's only one message, we shouldn't see the summary.
$assert_session->pageTextNotContains($expected_results[0]->getSummary());
$assert_session->pageTextContains((string) $expected_results[0]->getMessages()[0]);
// Ensure the update-ready form runs pre-commit checks immediately, even
// before it's submitted.
$expected_results = $this->testResults['checker_1']['1 error 1 warning'];
TestChecker1::setTestResult($expected_results, AutomaticUpdatesEvents::PRE_COMMIT);
$this->drupalGet('/admin/automatic-update-ready');
$assert_session->pageTextContains($expected_results['1:error']->getMessages()[0]);
// Only show errors, not warnings.
$assert_session->pageTextNotContains($expected_results['1:warning']->getMessages()[0]);
// Since there is only one error message, we shouldn't see the summary. And
// we shouldn't see the warning's summary in any case.
$assert_session->pageTextNotContains($expected_results['1:error']->getSummary());
$assert_session->pageTextNotContains($expected_results['1:warning']->getSummary());
// If a validator flags a warning, but doesn't throw, the update should
// continue.
$expected_results = $this->testResults['checker_1']['1 warning'];
TestChecker1::setTestResult($expected_results, AutomaticUpdatesEvents::PRE_START);
$session->reload();
$this->deleteStagedUpdate();
$page->pressButton('Download these updates');
$this->checkForMetaRefresh();
$assert_session->pageTextContains('Ready to update');
}
/**
* Tests that errors during the update process are displayed as messages.
* Deletes a staged, failed update.
*/
public function testBatchErrorsAreForwardedToMessenger(): void {
$this->setCoreVersion('9.8.0');
$error = ValidationResult::createError([
t('💥'),
], t('The update exploded.'));
TestUpdater::setBeginErrors([$error]);
$this->drupalLogin($this->rootUser);
$this->checkForUpdates();
$this->drupalGet('/admin/automatic-update');
$this->submitForm([], 'Download these updates');
$assert_session = $this->assertSession();
$assert_session->pageTextContains('An error has occurred.');
$this->getSession()->getPage()->clickLink('the error page');
$assert_session->pageTextContains('💥');
$assert_session->pageTextContains('The update exploded.');
private function deleteStagedUpdate(): void {
$session = $this->getSession();
$session->getPage()->pressButton('Delete existing update');
$this->assertSession()->pageTextContains('Staged update deleted');
$session->reload();
}
/**
......
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