Newer
Older
<?php
namespace Drupal\Tests\automatic_updates\Functional;
use Drupal\automatic_updates\Event\ReadinessCheckEvent;

Adam G-H
committed
use Drupal\package_manager\Event\PreCreateEvent;
use Drupal\package_manager\ValidationResult;
use Drupal\automatic_updates_test\ReadinessChecker\TestChecker1;
use Drupal\Tests\automatic_updates\Traits\ValidationTestTrait;

Ted Bowman
committed
use Drupal\Tests\package_manager\Traits\PackageManagerBypassTestTrait;
/**
* @covers \Drupal\automatic_updates\Form\UpdaterForm
*
* @group automatic_updates
*/

Adam G-H
committed
class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {

Kunal Sachdev
committed
use PackageManagerBypassTestTrait;

Ted Bowman
committed
use ValidationTestTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $modules = [

Kunal Sachdev
committed
'block',
'automatic_updates',
'automatic_updates_test',
'package_manager_bypass',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {

Ted Bowman
committed
// In this test class, all actual staging operations are bypassed by
// package_manager_bypass, which means this validator will complain because
// there is no actual Composer data for it to inspect.
$this->disableValidators[] = 'automatic_updates.staged_projects_validator';
parent::setUp();

Kunal Sachdev
committed
$this->setReleaseMetadata(__DIR__ . '/../../fixtures/release-history/drupal.9.8.1-security.xml');
$this->drupalLogin($this->rootUser);
$this->checkForUpdates();
}

Kunal Sachdev
committed
/**
* Data provider for URLs to the update form.

Kunal Sachdev
committed
*
* @return string[][]
* Test case parameters.
*/
public function providerUpdateFormReferringUrl(): array {

Kunal Sachdev
committed
return [
'Modules page' => ['/admin/modules/automatic-update'],
'Reports page' => ['/admin/reports/updates/automatic-update'],
];
}
/**
* Data provider for testTableLooksCorrect().
*
* @return string[][]
* Test case parameters.
*/
public function providerTableLooksCorrect(): array {

Kunal Sachdev
committed
return [
'Modules page' => ['modules'],
'Reports page' => ['reports'],
];
}
/**
* Tests that the form doesn't display any buttons if Drupal is up-to-date.
*
* @todo Mark this test as skipped if the web server is PHP's built-in, single
* threaded server.

Kunal Sachdev
committed
*
* @param string $update_form_url
* The URL of the update form to visit.

Kunal Sachdev
committed
*
* @dataProvider providerUpdateFormReferringUrl
public function testFormNotDisplayedIfAlreadyCurrent(string $update_form_url): void {
$this->setCoreVersion('9.8.1');

Kunal Sachdev
committed
$this->checkForUpdates();

Kunal Sachdev
committed
$this->drupalGet($update_form_url);
$assert_session = $this->assertSession();
$assert_session->statusCodeEquals(200);
$assert_session->pageTextContains('No update available');
$assert_session->buttonNotExists('Update');
}
/**
* Tests that available updates are rendered correctly in a table.

Kunal Sachdev
committed
*
* @param string $access_page
* The page from which the update form should be visited.
* Can be one of 'modules' to visit via the module list, or 'reports' to
* visit via the administrative reports page.

Kunal Sachdev
committed
*
* @dataProvider providerTableLooksCorrect

Kunal Sachdev
committed
public function testTableLooksCorrect(string $access_page): void {

Kunal Sachdev
committed
$this->drupalPlaceBlock('local_tasks_block', ['primary' => TRUE]);
$assert_session = $this->assertSession();
$this->setCoreVersion('9.8.0');

Ted Bowman
committed
$this->checkForUpdates();

Kunal Sachdev
committed

Kunal Sachdev
committed
// Navigate to the automatic updates form.
$this->drupalGet('/admin');

Kunal Sachdev
committed
if ($access_page === 'modules') {
$this->clickLink('Extend');
$assert_session->pageTextContainsOnce('There is a security update available for your version of Drupal.');
}
else {
$this->clickLink('Reports');
$assert_session->pageTextContainsOnce('There is a security update available for your version of Drupal.');
$this->clickLink('Available updates');
}

Ted Bowman
committed
$assert_session->pageTextNotContains('There is a security update available for your version of Drupal.');
$cells = $assert_session->elementExists('css', '#edit-projects .update-update-security')
->findAll('css', 'td');
$this->assertCount(3, $cells);
$assert_session->elementExists('named', ['link', 'Drupal'], $cells[0]);
$this->assertSame('9.8.0', $cells[1]->getText());
$this->assertSame('9.8.1 (Release notes)', $cells[2]->getText());
$release_notes = $assert_session->elementExists('named', ['link', 'Release notes'], $cells[2]);
$this->assertSame('Release notes for Drupal', $release_notes->getAttribute('title'));

Kunal Sachdev
committed
$assert_session->buttonExists('Update');

Ted Bowman
committed
$this->assertUpdateStagedTimes(0);
}
/**
* Tests handling of errors and warnings during the update process.
public function testUpdateErrors(): void {
$session = $this->getSession();
$assert_session = $this->assertSession();
$page = $session->getPage();
// Store a fake readiness error, which will be cached.
$message = t("You've not experienced Shakespeare until you have read him in the original Klingon.");
$error = ValidationResult::createError([$message]);
TestChecker1::setTestResult([$error], ReadinessCheckEvent::class);
$this->drupalGet('/admin/reports/status');
$page->clickLink('Run readiness checks');
$assert_session->pageTextContainsOnce((string) $message);
// Ensure that the fake error is cached.
$session->reload();
$assert_session->pageTextContainsOnce((string) $message);
$this->setCoreVersion('9.8.0');
$this->checkForUpdates();
// Set up a new fake error.
$this->createTestValidationResults();
$expected_results = $this->testResults['checker_1']['1 error'];
TestChecker1::setTestResult($expected_results, ReadinessCheckEvent::class);
// If a validator raises an error during readiness checking, the form should
// not have a submit button.

Kunal Sachdev
committed
$this->drupalGet('/admin/modules/automatic-update');
$assert_session->buttonNotExists('Update');
// Since this is an administrative page, the error message should be visible
// thanks to automatic_updates_page_top(). The readiness checks were re-run
// during the form build, which means the new error should be cached and
// displayed instead of the previously cached error.
$assert_session->pageTextContainsOnce((string) $expected_results[0]->getMessages()[0]);
$assert_session->pageTextContainsOnce(static::$errorsExplanation);
$assert_session->pageTextNotContains(static::$warningsExplanation);
$assert_session->pageTextNotContains((string) $message);
TestChecker1::setTestResult(NULL, ReadinessCheckEvent::class);

Adam G-H
committed
// Make the validator throw an exception during pre-create.
$error = new \Exception('The update exploded.');
TestChecker1::setException($error, PreCreateEvent::class);
$session->reload();
$assert_session->pageTextNotContains(static::$errorsExplanation);
$assert_session->pageTextNotContains(static::$warningsExplanation);
$page->pressButton('Update');
$this->checkForMetaRefresh();

Ted Bowman
committed
$this->assertUpdateStagedTimes(0);
$assert_session->pageTextContainsOnce('An error has occurred.');
$page->clickLink('the error page');

Adam G-H
committed
// We should see the exception message, but not the validation result's
// messages or summary, because exceptions thrown directly by event
// subscribers are wrapped in simple exceptions and re-thrown.
$assert_session->pageTextContainsOnce($error->getMessage());
$assert_session->pageTextNotContains((string) $expected_results[0]->getMessages()[0]);
$assert_session->pageTextNotContains($expected_results[0]->getSummary());

Adam G-H
committed
// Since the error occurred during pre-create, there should be no existing
// update to delete.

Ted Bowman
committed
$assert_session->buttonNotExists('Delete existing update');

Adam G-H
committed
// If a validator flags an error, but doesn't throw, the update should still
// be halted.

Adam G-H
committed
TestChecker1::setTestResult($expected_results, PreCreateEvent::class);
$page->pressButton('Update');
$this->checkForMetaRefresh();

Ted Bowman
committed
$this->assertUpdateStagedTimes(0);
$assert_session->pageTextContainsOnce('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->pageTextContainsOnce((string) $expected_results[0]->getMessages()[0]);
}

Kunal Sachdev
committed
/**
* Tests that updating to a different minor version isn't supported.

Kunal Sachdev
committed
*
* @param string $update_form_url
* The URL of the update form to visit.

Kunal Sachdev
committed
*
* @dataProvider providerUpdateFormReferringUrl

Kunal Sachdev
committed
*/
public function testMinorVersionUpdateNotSupported(string $update_form_url): void {

Kunal Sachdev
committed
$this->setCoreVersion('9.7.1');

Kunal Sachdev
committed
$this->checkForUpdates();

Kunal Sachdev
committed
$this->drupalGet($update_form_url);

Kunal Sachdev
committed
$assert_session = $this->assertSession();

Kunal Sachdev
committed
$assert_session->pageTextContainsOnce('Drupal cannot be automatically updated from its current version, 9.7.1, to the recommended version, 9.8.1, because automatic updates from one minor version to another are not supported.');

Kunal Sachdev
committed
$assert_session->buttonNotExists('Update');
}
/**

Ted Bowman
committed
* Tests deleting an existing update.
*
* @todo Add test coverage for differences between stage owner and other users
* in https://www.drupal.org/i/3248928.

Ted Bowman
committed
public function testDeleteExistingUpdate() {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->setCoreVersion('9.8.0');
$this->checkForUpdates();
$this->drupalGet('/admin/modules/automatic-update');
$page->pressButton('Update');
$this->checkForMetaRefresh();

Ted Bowman
committed
$this->assertUpdateStagedTimes(1);

Ted Bowman
committed
// Confirm we are on the confirmation page.
$this->assertUpdateReady();

Ted Bowman
committed
$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->pageTextContains('Staged update deleted');

Ted Bowman
committed
$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.
$this->assertUpdateReady();

Ted Bowman
committed
$this->assertUpdateStagedTimes(2);

Ted Bowman
committed
$assert_session->buttonExists('Continue');
// Cancel the update, then ensure that we are bounced back to the start
// page, and that it will allow us to begin the update anew.
$page->pressButton('Cancel update');
$assert_session->addressEquals('/admin/reports/updates/automatic-update');
$assert_session->pageTextContains('The update was successfully cancelled.');
$assert_session->buttonExists('Update');
}

Adam G-H
committed
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
/**
* Tests the update form when staged modules have database updates.
*/
public function testStagedDatabaseUpdates(): void {
$this->setCoreVersion('9.8.0');
$this->checkForUpdates();
// Simulate a staged database update in the System module.
$this->container->get('state')
->set('automatic_updates_test.staged_database_updates', [
'system' => [
'name' => 'System',
],
]);
$page = $this->getSession()->getPage();
$this->drupalGet('/admin/modules/automatic-update');
$page->pressButton('Update');
$this->checkForMetaRefresh();
$this->assertUpdateStagedTimes(1);
$this->assertUpdateReady();
// We should see a warning about pending database updates, and once the
// staged changes have been applied, we should be redirected to update.php.
$assert_session = $this->assertSession();
$possible_update_message = 'Possible database updates were detected in the following modules; you may be redirected to the database update page in order to complete the update process.';
$assert_session->pageTextContains($possible_update_message);
$assert_session->pageTextContains('System');
$page->pressButton('Continue');
$this->checkForMetaRefresh();
$assert_session->addressEquals('/update.php');
$assert_session->pageTextNotContains($possible_update_message);
$assert_session->pageTextContainsOnce('Please apply database updates to complete the update process.');
}
/**
* Tests an update that has no errors or special conditions.
*
* @param string $update_form_url
* The URL of the update form to visit.
*
* @dataProvider providerUpdateFormReferringUrl
*/
public function testSuccessfulUpdate(string $update_form_url): void {
$this->setCoreVersion('9.8.0');
$this->checkForUpdates();
$page = $this->getSession()->getPage();
$this->drupalGet($update_form_url);
$page->pressButton('Update');
$this->checkForMetaRefresh();
$this->assertUpdateStagedTimes(1);
$this->assertUpdateReady();
$assert_session = $this->assertSession();
$page->pressButton('Continue');
$this->checkForMetaRefresh();
$assert_session->addressEquals('/admin/reports/updates');
$assert_session->pageTextContainsOnce('Update complete!');
}