first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Functional;
/**
* Tests the Update Manager module upload via authorize.php functionality.
*
* @group update
*/
class FileTransferAuthorizeFormTest extends UpdateUploaderTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$admin_user = $this->drupalCreateUser([
'administer modules',
'administer software updates',
]);
$this->drupalLogin($admin_user);
// Create a local cache so the module is not downloaded from drupal.org.
$cache_directory = _update_manager_cache_directory(TRUE);
foreach (['.tar.gz', '.zip'] as $extension) {
$filename = 'update_test_new_module' . $extension;
copy(
__DIR__ . '/../../update_test_new_module/8.x-1.0/' . $filename,
$cache_directory . '/' . $filename
);
}
}
/**
* Tests the Update Manager module upload via authorize.php functionality.
*
* @dataProvider archiveFileUrlProvider
*/
public function testViaAuthorize($url): void {
// Ensure the that we can select which file transfer backend to use.
\Drupal::state()->set('test_uploaders_via_prompt', TRUE);
// Ensure the module does not already exist.
$this->drupalGet('admin/modules');
$this->assertSession()->pageTextNotContains('Update test new module');
$edit = [
'project_url' => $url,
];
$this->drupalGet('admin/modules/install');
$this->submitForm($edit, 'Continue');
$edit = [
'connection_settings[authorize_filetransfer_default]' => 'system_test',
'connection_settings[system_test][update_test_username]' => $this->randomMachineName(),
];
$this->submitForm($edit, 'Continue');
$this->assertSession()->pageTextContains('Files were added successfully.');
// Ensure the module is available to install.
$this->drupalGet('admin/modules');
$this->assertSession()->pageTextContains('Update test new module');
}
/**
* Data provider method for testViaAuthorize().
*
* Each of these release URLs has been cached in the setUp() method.
*/
public static function archiveFileUrlProvider() {
return [
'tar.gz' => [
'url' => 'https://ftp.drupal.org/files/projects/update_test_new_module.tar.gz',
],
'zip' => [
'url' => 'https://ftp.drupal.org/files/projects/update_test_new_module.zip',
],
];
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for update.
*
* @group update
*/
class GenericTest extends GenericModuleTestBase {}

View File

@@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Functional\Update;
use Drupal\Core\Database\Database;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
use Drupal\Tests\UpdatePathTestTrait;
/**
* Tests update of update.settings:fetch.url if it's still the default of "".
*
* @group system
* @covers \update_post_update_set_blank_fetch_url_to_null
*/
class UpdateSettingsDefaultFetchUrlUpdateTest extends UpdatePathTestBase {
use UpdatePathTestTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
DRUPAL_ROOT . '/core/modules/system/tests/fixtures/update/drupal-9.4.0.bare.standard.php.gz',
];
}
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$connection = Database::getConnection();
// Set the schema version.
$connection->merge('key_value')
->fields([
'value' => 'i:8001;',
'name' => 'update',
'collection' => 'system.schema',
])
->condition('collection', 'system.schema')
->condition('name', 'update')
->execute();
// Update core.extension.
$extensions = $connection->select('config')
->fields('config', ['data'])
->condition('collection', '')
->condition('name', 'core.extension')
->execute()
->fetchField();
$extensions = unserialize($extensions);
$extensions['module']['update'] = 0;
$connection->update('config')
->fields(['data' => serialize($extensions)])
->condition('collection', '')
->condition('name', 'core.extension')
->execute();
// Create update.settings config.
$default_update_settings = [
'check' => [
'disabled_extensions' => FALSE,
'interval_days' => 1,
],
'fetch' => [
'url' => '',
'max_attempts' => 2,
'timeout' => 30,
],
'notification' => [
'emails' => [],
'threshold' => 'all',
],
];
$connection->insert('config')
->fields([
'collection',
'name',
'data',
])
->values([
'collection' => '',
'name' => 'update.settings',
'data' => serialize($default_update_settings),
])
->execute();
}
/**
* Tests update of update.settings:fetch.url.
*/
public function testUpdate(): void {
$fetch_url_before = $this->config('update.settings')->get('fetch.url');
$this->assertSame('', $fetch_url_before);
$this->runUpdates();
$fetch_url_after = $this->config('update.settings')->get('fetch.url');
$this->assertNull($fetch_url_after);
}
}

View File

@@ -0,0 +1,862 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Functional;
use Drupal\Core\Utility\ProjectInfo;
use Drupal\update\UpdateManagerInterface;
/**
* Tests how the Update Manager handles contributed modules and themes.
*
* @group update
* @group #slow
*/
class UpdateContribTest extends UpdateTestBase {
use UpdateTestTrait;
/**
* {@inheritdoc}
*/
protected $updateTableLocator = 'table.update:nth-of-type(2)';
/**
* {@inheritdoc}
*/
protected $updateProject = 'aaa_update_test';
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'aaa_update_test',
'bbb_update_test',
'ccc_update_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$admin_user = $this->drupalCreateUser(['administer site configuration']);
$this->drupalLogin($admin_user);
}
/**
* Tests when there is no available release data for a contrib module.
*/
public function testNoReleasesAvailable(): void {
$this->mockInstalledExtensionsInfo([
'aaa_update_test' => [
'project' => 'aaa_update_test',
'version' => '8.x-1.0',
'hidden' => FALSE,
],
]);
$this->mockDefaultExtensionsInfo(['version' => '8.0.0']);
$this->refreshUpdateStatus(['drupal' => '8.0.0', 'aaa_update_test' => 'no-releases']);
// Cannot use $this->standardTests() because we need to check for the
// 'No available releases found' string.
$this->assertSession()->responseContains('<h3>Drupal core</h3>');
$this->assertSession()->linkExists('Drupal');
$this->assertSession()->linkByHrefExists('http://example.com/project/drupal');
$this->assertSession()->pageTextContains('Up to date');
$this->assertSession()->responseContains('<h3>Modules</h3>');
$this->assertSession()->pageTextNotContains('Update available');
$this->assertSession()->pageTextContains('No available releases found');
$this->assertSession()->linkNotExists('AAA Update test');
$this->assertSession()->linkByHrefNotExists('http://example.com/project/aaa_update_test');
$available = update_get_available();
$this->assertFalse(isset($available['aaa_update_test']['fetch_status']), 'Results are cached even if no releases are available.');
}
/**
* Tests the basic functionality of a contrib module on the status report.
*/
public function testUpdateContribBasic(): void {
$installed_extensions = [
'aaa_update_test' => [
'project' => 'aaa_update_test',
'version' => '8.x-1.0',
'hidden' => FALSE,
],
];
$this->mockInstalledExtensionsInfo($installed_extensions);
$this->mockDefaultExtensionsInfo(['version' => '8.0.0']);
$this->refreshUpdateStatus(
[
'drupal' => '8.0.0',
'aaa_update_test' => '1_0',
]
);
$this->standardTests();
$this->assertSession()->pageTextContains('Up to date');
$this->assertSession()->responseContains('<h3>Modules</h3>');
$this->assertSession()->pageTextNotContains('Update available');
$this->assertSession()->linkExists('AAA Update test');
$this->assertSession()->linkByHrefExists('http://example.com/project/aaa_update_test');
// Since aaa_update_test is installed the fact it is hidden and in the
// Testing package means it should not appear.
$installed_extensions['aaa_update_test']['hidden'] = TRUE;
$this->mockInstalledExtensionsInfo($installed_extensions);
$this->mockDefaultExtensionsInfo(['version' => '8.0.0']);
$this->refreshUpdateStatus(
[
'drupal' => '8.0.0',
'aaa_update_test' => '1_0',
]
);
$this->assertSession()->linkNotExists('AAA Update test');
$this->assertSession()->linkByHrefNotExists('http://example.com/project/aaa_update_test');
// A hidden and installed project not in the Testing package should appear.
$installed_extensions['aaa_update_test']['package'] = 'aaa_update_test';
$this->mockInstalledExtensionsInfo($installed_extensions);
$this->mockDefaultExtensionsInfo(['version' => '8.0.0']);
$this->refreshUpdateStatus(
[
'drupal' => '8.0.0',
'aaa_update_test' => '1_0',
]
);
$this->assertSession()->linkExists('AAA Update test');
$this->assertSession()->linkByHrefExists('http://example.com/project/aaa_update_test');
}
/**
* Tests that contrib projects are ordered by project name.
*
* If a project contains multiple modules, we want to make sure that the
* available updates report is sorted by the parent project names, not by the
* names of the modules included in each project. In this test case, we have
* two contrib projects, "BBB Update test" and "CCC Update test". However, we
* have a module called "aaa_update_test" that's part of the "CCC Update test"
* project. We need to make sure that we see the "BBB" project before the
* "CCC" project, even though "CCC" includes a module that's processed first
* if you sort alphabetically by module name (which is the order we see things
* inside \Drupal\Core\Extension\ExtensionList::getList() for example).
*/
public function testUpdateContribOrder(): void {
// We want core to be version 8.0.0.
$this->mockDefaultExtensionsInfo(['version' => '8.0.0']);
// All the rest should be visible as contrib modules at version 8.x-1.0.
$this->mockInstalledExtensionsInfo([
// aaa_update_test needs to be part of the "CCC Update test" project,
// which would throw off the report if we weren't properly sorting by
// the project names.
'aaa_update_test' => [
'project' => 'ccc_update_test',
'version' => '8.x-1.0',
'hidden' => FALSE,
],
// This should be its own project, and listed first on the report.
'bbb_update_test' => [
'project' => 'bbb_update_test',
'version' => '8.x-1.0',
'hidden' => FALSE,
],
// This will contain both aaa_update_test and ccc_update_test, and
// should come after the bbb_update_test project.
'ccc_update_test' => [
'project' => 'ccc_update_test',
'version' => '8.x-1.0',
'hidden' => FALSE,
],
]);
$this->refreshUpdateStatus(['drupal' => '8.0.0', '#all' => '1_0']);
$this->standardTests();
// We're expecting the report to say all projects are up to date.
$this->assertSession()->pageTextContains('Up to date');
$this->assertSession()->pageTextNotContains('Update available');
// We want to see all 3 module names listed, since they'll show up either
// as project names or as modules under the "Includes" listing.
$this->assertSession()->pageTextContains('AAA Update test');
$this->assertSession()->pageTextContains('BBB Update test');
$this->assertSession()->pageTextContains('CCC Update test');
// We want aaa_update_test included in the ccc_update_test project, not as
// its own project on the report.
$this->assertSession()->linkNotExists('AAA Update test');
$this->assertSession()->linkByHrefNotExists('http://example.com/project/aaa_update_test');
// The other two should be listed as projects.
$this->assertSession()->linkExists('BBB Update test');
$this->assertSession()->linkByHrefExists('http://example.com/project/bbb_update_test');
$this->assertSession()->linkExists('CCC Update test');
$this->assertSession()->linkByHrefExists('http://example.com/project/ccc_update_test');
// We want to make sure we see the BBB project before the CCC project.
// Instead of just searching for 'BBB Update test' or something, we want
// to use the full markup that starts the project entry itself, so that
// we're really testing that the project listings are in the right order.
$bbb_project_link = '<div class="project-update__title"><a href="http://example.com/project/bbb_update_test">BBB Update test</a>';
$ccc_project_link = '<div class="project-update__title"><a href="http://example.com/project/ccc_update_test">CCC Update test</a>';
// Verify that the 'BBB Update test' project is listed before the
// 'CCC Update test' project.
$this->assertLessThan(strpos($this->getSession()->getPage()->getContent(), $ccc_project_link), strpos($this->getSession()->getPage()->getContent(), $bbb_project_link));
}
/**
* Tests that subthemes are notified about security updates for base themes.
*/
public function testUpdateBaseThemeSecurityUpdate(): void {
// @todo https://www.drupal.org/node/2338175 base themes have to be
// installed.
// Only install the subtheme, not the base theme.
\Drupal::service('theme_installer')->install(['update_test_subtheme']);
// Define the initial state for core and the subtheme.
$this->mockInstalledExtensionsInfo([
// Show the update_test_basetheme.
'update_test_basetheme' => [
'project' => 'update_test_basetheme',
'version' => '8.x-1.0',
'hidden' => FALSE,
],
// Show the update_test_subtheme.
'update_test_subtheme' => [
'project' => 'update_test_subtheme',
'version' => '8.x-1.0',
'hidden' => FALSE,
],
]);
$xml_mapping = [
'drupal' => '8.0.0',
'update_test_subtheme' => '1_0',
'update_test_basetheme' => '1_1-sec',
];
$this->refreshUpdateStatus($xml_mapping);
$this->assertSession()->pageTextContains('Security update required!');
$this->updateProject = 'update_test_basetheme';
$this->assertVersionUpdateLinks('Security update', '8.x-1.1');
}
/**
* Tests the Update Manager module when one normal update is available.
*/
public function testNormalUpdateAvailable(): void {
$assert_session = $this->assertSession();
// Ensure that the update check requires a token.
$this->drupalGet('admin/reports/updates/check');
$assert_session->statusCodeEquals(403);
$this->mockInstalledExtensionsInfo([
'aaa_update_test' => [
'project' => 'aaa_update_test',
'version' => '8.x-1.0',
'hidden' => FALSE,
],
]);
foreach (['1.1', '1.2', '2.0'] as $version) {
foreach (['-beta1', '-alpha1', ''] as $extra_version) {
$full_version = "8.x-$version$extra_version";
$this->refreshUpdateStatus([
'drupal' => '8.0.0',
'aaa_update_test' => str_replace('.', '_', $version) . $extra_version,
]);
$this->standardTests();
$assert_session->pageTextNotContains('Security update required!');
// The XML test fixtures for this method all contain the '8.x-3.0'
// release but because '8.x-3.0' is not in a supported branch it will
// not be in the available updates.
$this->assertSession()->responseNotContains('8.x-3.0');
// Set a CSS selector in order for assertions to target the 'Modules'
// table and not Drupal core updates.
$this->updateTableLocator = 'table.update:nth-of-type(2)';
switch ($version) {
case '1.1':
// Both stable and unstable releases are available.
// A stable release is the latest.
if ($extra_version == '') {
$assert_session->elementTextNotContains('css', $this->updateTableLocator, 'Up to date');
$assert_session->elementTextContains('css', $this->updateTableLocator, 'Update available');
$this->assertVersionUpdateLinks('Recommended version', $full_version);
$assert_session->elementTextNotContains('css', $this->updateTableLocator, 'Latest version:');
$assert_session->elementContains('css', $this->updateTableLocator, 'warning.svg');
}
// Only unstable releases are available.
// An unstable release is the latest.
else {
$assert_session->elementTextContains('css', $this->updateTableLocator, 'Up to date');
$assert_session->elementTextNotContains('css', $this->updateTableLocator, 'Update available');
$assert_session->elementTextNotContains('css', $this->updateTableLocator, 'Recommended version:');
$this->assertVersionUpdateLinks('Latest version', $full_version);
$assert_session->elementContains('css', $this->updateTableLocator, 'check.svg');
}
break;
case '1.2':
// Both stable and unstable releases are available.
// A stable release is the latest.
if ($extra_version == '') {
$assert_session->elementTextNotContains('css', $this->updateTableLocator, 'Up to date');
$assert_session->elementTextContains('css', $this->updateTableLocator, 'Update available');
$this->assertVersionUpdateLinks('Recommended version:', $full_version);
$assert_session->elementTextNotContains('css', $this->updateTableLocator, 'Latest version:');
$assert_session->elementContains('css', $this->updateTableLocator, 'warning.svg');
}
// Both stable and unstable releases are available.
// An unstable release is the latest.
else {
$assert_session->elementTextNotContains('css', $this->updateTableLocator, 'Up to date');
$assert_session->elementTextContains('css', $this->updateTableLocator, 'Update available');
$this->assertVersionUpdateLinks('Recommended version:', '8.x-1.1');
$this->assertVersionUpdateLinks('Latest version:', $full_version);
$assert_session->elementTextContains('css', $this->updateTableLocator, 'Latest version:');
$assert_session->elementContains('css', $this->updateTableLocator, 'warning.svg');
}
break;
case '2.0':
// When next major release (either stable or unstable) is available
// and the current major is still supported, the next major will be
// listed as "Also available".
$assert_session->elementTextNotContains('css', $this->updateTableLocator, 'Up to date');
$assert_session->elementTextContains('css', $this->updateTableLocator, 'Update available');
$this->assertVersionUpdateLinks('Recommended version', '8.x-1.2');
$this->assertVersionUpdateLinks('Also available', $full_version);
$assert_session->elementTextNotContains('css', $this->updateTableLocator, 'Latest version:');
$assert_session->elementContains('css', $this->updateTableLocator, 'warning.svg');
}
}
}
}
/**
* Tests that uninstalled themes are only shown when desired.
*
* @todo https://www.drupal.org/node/2338175 extensions can not be hidden and
* base themes have to be installed.
*/
public function testUpdateShowDisabledThemes(): void {
$update_settings = $this->config('update.settings');
// Make sure all the update_test_* themes are uninstalled.
$extension_config = $this->config('core.extension');
foreach ($extension_config->get('theme') as $theme => $weight) {
if (str_starts_with($theme, 'update_test_')) {
$extension_config->clear("theme.$theme");
}
}
$extension_config->save();
// Define the initial state for core and the test contrib themes.
$this->mockInstalledExtensionsInfo([
// The update_test_basetheme should be visible and up to date.
'update_test_basetheme' => [
'project' => 'update_test_basetheme',
'version' => '8.x-1.1',
'hidden' => FALSE,
],
// The update_test_subtheme should be visible and up to date.
'update_test_subtheme' => [
'project' => 'update_test_subtheme',
'version' => '8.x-1.0',
'hidden' => FALSE,
],
]);
// When there are contributed modules in the site's file system, the
// total number of attempts made in the test may exceed the default value
// of update_max_fetch_attempts. Therefore this variable is set very high
// to avoid test failures in those cases.
$update_settings->set('fetch.max_attempts', 99999)->save();
$xml_mapping = [
'drupal' => '8.0.0',
'update_test_subtheme' => '1_0',
'update_test_basetheme' => '1_1-sec',
];
foreach ([TRUE, FALSE] as $check_disabled) {
$update_settings->set('check.disabled_extensions', $check_disabled)->save();
$this->refreshUpdateStatus($xml_mapping);
// In neither case should we see the "Themes" heading for installed
// themes.
// Use regex pattern because we need to match 'Themes' case sensitively.
$this->assertSession()->pageTextNotMatches('/Themes/');
if ($check_disabled) {
$this->assertSession()->pageTextContains('Uninstalled themes');
$this->assertSession()->linkExists('Update test base theme');
$this->assertSession()->linkByHrefExists('http://example.com/project/update_test_basetheme');
$this->assertSession()->linkExists('Update test subtheme');
$this->assertSession()->linkByHrefExists('http://example.com/project/update_test_subtheme');
}
else {
$this->assertSession()->pageTextNotContains('Uninstalled themes');
$this->assertSession()->linkNotExists('Update test base theme');
$this->assertSession()->linkByHrefNotExists('http://example.com/project/update_test_basetheme');
$this->assertSession()->linkNotExists('Update test subtheme');
$this->assertSession()->linkByHrefNotExists('http://example.com/project/update_test_subtheme');
}
}
}
/**
* Tests updates with a hidden base theme.
*/
public function testUpdateHiddenBaseTheme(): void {
\Drupal::moduleHandler()->loadInclude('update', 'inc', 'update.compare');
// Install the subtheme.
\Drupal::service('theme_installer')->install(['update_test_subtheme']);
// Add a project and initial state for base theme and subtheme.
$this->mockInstalledExtensionsInfo([
// Hide the update_test_basetheme.
'update_test_basetheme' => [
'project' => 'update_test_basetheme',
'hidden' => TRUE,
],
// Show the update_test_subtheme.
'update_test_subtheme' => [
'project' => 'update_test_subtheme',
'hidden' => FALSE,
],
]);
$projects = \Drupal::service('update.manager')->getProjects();
$theme_data = \Drupal::service('extension.list.theme')->reset()->getList();
$project_info = new ProjectInfo();
$project_info->processInfoList($projects, $theme_data, 'theme', TRUE);
$this->assertNotEmpty($projects['update_test_basetheme'], 'Valid base theme (update_test_basetheme) was found.');
}
/**
* Makes sure that if we fetch from a broken URL, sane things happen.
*/
public function testUpdateBrokenFetchURL(): void {
$this->mockInstalledExtensionsInfo([
'aaa_update_test' => [
'project' => 'aaa_update_test',
'version' => '8.x-1.0',
'hidden' => FALSE,
],
'bbb_update_test' => [
'project' => 'bbb_update_test',
'version' => '8.x-1.0',
'hidden' => FALSE,
],
'ccc_update_test' => [
'project' => 'ccc_update_test',
'version' => '8.x-1.0',
'hidden' => FALSE,
],
]);
$this->mockDefaultExtensionsInfo(['version' => '8.0.0']);
// Ensure that the update information is correct before testing.
$this->drupalGet('admin/reports/updates');
$xml_mapping = [
'drupal' => '8.0.0',
'aaa_update_test' => '1_0',
'bbb_update_test' => 'does-not-exist',
'ccc_update_test' => '1_0',
];
$this->refreshUpdateStatus($xml_mapping);
$this->assertSession()->pageTextContains('Up to date');
// We're expecting the report to say most projects are up to date, so we
// hope that 'Up to date' is not unique.
$this->assertSession()->pageTextMatchesCount(3, '/Up to date/');
// It should say we failed to get data, not that we're missing an update.
$this->assertSession()->pageTextNotContains('Update available');
// We need to check that this string is found as part of a project row, not
// just in the "Failed to get available update data" message at the top of
// the page.
$this->assertSession()->responseContains('<div class="project-update__status">Failed to get available update data');
// We should see the output messages from fetching manually.
$this->assertSession()->pageTextContainsOnce('Checked available update data for 3 projects.');
$this->assertSession()->pageTextContainsOnce('Failed to get available update data for one project.');
// The other two should be listed as projects.
$this->assertSession()->linkExists('AAA Update test');
$this->assertSession()->linkByHrefExists('http://example.com/project/aaa_update_test');
$this->assertSession()->linkNotExists('BBB Update test');
$this->assertSession()->linkByHrefNotExists('http://example.com/project/bbb_update_test');
$this->assertSession()->linkExists('CCC Update test');
$this->assertSession()->linkByHrefExists('http://example.com/project/ccc_update_test');
}
/**
* Checks that hook_update_status_alter() works to change a status.
*
* We provide the same external data as if aaa_update_test 8.x-1.0 were
* installed and that was the latest release. Then we use
* hook_update_status_alter() to try to mark this as missing a security
* update, then assert if we see the appropriate warnings on the right pages.
*/
public function testHookUpdateStatusAlter(): void {
$update_admin_user = $this->drupalCreateUser([
'administer site configuration',
'administer software updates',
]);
$this->drupalLogin($update_admin_user);
$this->mockInstalledExtensionsInfo([
'aaa_update_test' => [
'project' => 'aaa_update_test',
'version' => '8.x-1.0',
'hidden' => FALSE,
],
]);
$this->mockDefaultExtensionsInfo(['version' => '8.0.0']);
$update_test_config = $this->config('update_test.settings');
$update_status = [
'aaa_update_test' => [
'status' => UpdateManagerInterface::NOT_SECURE,
],
];
$update_test_config->set('update_status', $update_status)->save();
$this->refreshUpdateStatus(
[
'drupal' => '8.0.0',
'aaa_update_test' => '1_0',
]
);
$this->assertSession()->responseContains('<h3>Modules</h3>');
$this->assertSession()->pageTextContains('Security update required!');
$this->assertSession()->linkExists('AAA Update test');
$this->assertSession()->linkByHrefExists('http://example.com/project/aaa_update_test');
// Visit the reports page again without the altering and make sure the
// status is back to normal.
$update_test_config->set('update_status', [])->save();
$this->drupalGet('admin/reports/updates');
$this->assertSession()->responseContains('<h3>Modules</h3>');
$this->assertSession()->pageTextNotContains('Security update required!');
$this->assertSession()->linkExists('AAA Update test');
$this->assertSession()->linkByHrefExists('http://example.com/project/aaa_update_test');
// Turn the altering back on and visit the Update manager UI.
$update_test_config->set('update_status', $update_status)->save();
$this->drupalGet('admin/modules/update');
$this->assertSession()->pageTextContains('Security update');
// Turn the altering back off and visit the Update manager UI.
$update_test_config->set('update_status', [])->save();
$this->drupalGet('admin/modules/update');
$this->assertSession()->pageTextNotContains('Security update');
}
/**
* Tests that core compatibility messages are displayed.
*/
public function testCoreCompatibilityMessage(): void {
$this->mockInstalledExtensionsInfo([
'aaa_update_test' => [
'project' => 'aaa_update_test',
'version' => '8.x-1.0',
'hidden' => FALSE,
],
]);
$this->mockDefaultExtensionsInfo(['version' => '8.0.0']);
$this->refreshUpdateStatus(['drupal' => '8.1.1', 'aaa_update_test' => '8.x-1.2']);
$this->assertCoreCompatibilityMessage('8.x-1.2', '8.0.0 to 8.1.1', 'Recommended version:');
$this->assertCoreCompatibilityMessage('8.x-1.3-beta1', '8.0.0, 8.1.1', 'Latest version:');
// Run the same check as above but with a Drupal core XML test fixture
// without '8.1.' in 'supported_branches'. Confirm that messages do not
// include releases from the '8.1.' branch.
$this->refreshUpdateStatus(['drupal' => '8.1.1-core_compatibility', 'aaa_update_test' => '8.x-1.2']);
$this->assertCoreCompatibilityMessage('8.x-1.2', '8.0.0 to 8.0.1', 'Recommended version:');
$this->assertCoreCompatibilityMessage('8.x-1.3-beta1', '8.0.0', 'Latest version:');
// Change the available core releases and confirm that the messages change.
$this->refreshUpdateStatus(['drupal' => '8.1.1-alpha1', 'aaa_update_test' => '8.x-1.2']);
$this->assertCoreCompatibilityMessage('8.x-1.2', '8.0.0 to 8.1.0', 'Recommended version:');
$this->assertCoreCompatibilityMessage('8.x-1.3-beta1', '8.0.0', 'Latest version:');
// Confirm that messages are displayed for security and 'Also available'
// updates.
$this->refreshUpdateStatus(['drupal' => '8.1.1', 'aaa_update_test' => 'core_compatibility.8.x-1.2_8.x-2.2']);
$this->assertCoreCompatibilityMessage('8.x-1.2', '8.1.0 to 8.1.1', 'Security update:', FALSE);
$this->assertCoreCompatibilityMessage('8.x-2.2', '8.1.1', 'Also available:', FALSE);
}
/**
* Tests update status of security releases.
*
* @param string $module_version
* The module version the site is using.
* @param string[] $expected_security_releases
* The security releases, if any, that the status report should recommend.
* @param string $expected_update_message_type
* The type of update message expected.
* @param string $fixture
* The fixture file to use.
*
* @dataProvider securityUpdateAvailabilityProvider
*/
public function testSecurityUpdateAvailability($module_version, array $expected_security_releases, $expected_update_message_type, $fixture): void {
$this->mockInstalledExtensionsInfo([
'aaa_update_test' => [
'project' => 'aaa_update_test',
'version' => $module_version,
'hidden' => FALSE,
],
]);
$this->mockDefaultExtensionsInfo(['version' => '8.0.0']);
$this->refreshUpdateStatus(['drupal' => '8.0.0', 'aaa_update_test' => $fixture]);
$this->assertSecurityUpdates('aaa_update_test', $expected_security_releases, $expected_update_message_type, 'table.update:nth-of-type(2)');
}
/**
* Data provider method for testSecurityUpdateAvailability().
*
* These test cases rely on the following fixtures containing the following
* releases:
* - aaa_update_test.sec.8.x-1.2.xml
* - 8.x-1.2 Security update
* - 8.x-1.1 Insecure
* - 8.x-1.0 Insecure
* - aaa_update_test.sec.8.x-1.1_8.x-1.2.xml
* - 8.x-1.2 Security update
* - 8.x-1.1 Security update, Insecure
* - 8.x-1.0 Insecure
* - aaa_update_test.sec.8.x-1.2_8.x-2.2.xml
* - 8.x-3.0-beta2
* - 8.x-3.0-beta1 Insecure
* - 8.x-2.2 Security update
* - 8.x-2.1 Security update, Insecure
* - 8.x-2.0 Insecure
* - 8.x-1.2 Security update
* - 8.x-1.1 Insecure
* - 8.x-1.0 Insecure
* - aaa_update_test.sec.8.x-2.2_1.x_secure.xml
* - 8.x-2.2 Security update
* - 8.x-2.1 Security update, Insecure
* - 8.x-2.0 Insecure
* - 8.x-1.2
* - 8.x-1.1
* - 8.x-1.0
*/
public static function securityUpdateAvailabilityProvider() {
return [
// Security releases available for module major release 1.
// No releases for next major.
'8.x-1.0, 8.x-1.2' => [
'module_version' => '8.x-1.0',
'expected_security_releases' => ['8.x-1.2'],
'expected_update_message_type' => static::SECURITY_UPDATE_REQUIRED,
'fixture' => 'sec.8.x-1.2',
],
// Two security releases available for module major release 1.
// 8.x-1.1 security release marked as insecure.
// No releases for next major.
'8.x-1.0, 8.x-1.1 8.x-1.2' => [
'module_version' => '8.x-1.0',
'expected_security_releases' => ['8.x-1.2'],
'expected_update_message_type' => static::SECURITY_UPDATE_REQUIRED,
'fixture' => 'sec.8.x-1.1_8.x-1.2',
],
// Security release available for module major release 2.
// No releases for next major.
'8.x-2.0, 8.x-2.2' => [
'module_version' => '8.x-2.0',
'expected_security_releases' => ['8.x-2.2'],
'expected_update_message_type' => static::SECURITY_UPDATE_REQUIRED,
'fixture' => 'sec.8.x-2.2_1.x_secure',
],
'8.x-2.2, 8.x-1.2 8.x-2.2' => [
'module_version' => '8.x-2.2',
'expected_security_releases' => [],
'expected_update_message_type' => static::UPDATE_NONE,
'fixture' => 'sec.8.x-1.2_8.x-2.2',
],
// Security release available for module major release 1.
// Security release also available for next major.
'8.x-1.0, 8.x-1.2 8.x-2.2' => [
'module_version' => '8.x-1.0',
'expected_security_releases' => ['8.x-1.2'],
'expected_update_message_type' => static::SECURITY_UPDATE_REQUIRED,
'fixture' => 'sec.8.x-1.2_8.x-2.2',
],
// No security release available for module major release 1 but 1.x
// releases are not marked as insecure.
// Security release available for next major.
'8.x-1.0, 8.x-2.2, not insecure' => [
'module_version' => '8.x-1.0',
'expected_security_releases' => [],
'expected_update_message_type' => static::UPDATE_AVAILABLE,
'fixture' => 'sec.8.x-2.2_1.x_secure',
],
// On latest security release for module major release 1.
// Security release also available for next major.
'8.x-1.2, 8.x-1.2 8.x-2.2' => [
'module_version' => '8.x-1.2',
'expected_security_releases' => [],
'expected_update_message_type' => static::UPDATE_NONE,
'fixture' => 'sec.8.x-1.2_8.x-2.2',
],
'8.x-2.0, 8.x-1.2 8.x-2.2' => [
'module_version' => '8.x-2.0',
'expected_security_releases' => ['8.x-2.2'],
'expected_update_message_type' => static::SECURITY_UPDATE_REQUIRED,
'fixture' => 'sec.8.x-1.2_8.x-2.2',
],
// @todo In https://www.drupal.org/node/2865920 add test cases:
// - 8.x-3.0-beta1 using fixture 'sec.8.x-1.2_8.x-2.2' to ensure that
// 8.x-2.2 is the only security update.
];
}
/**
* Tests messages when a project release is unpublished.
*
* This test confirms that revoked messages are displayed regardless of
* whether the installed version is in a supported branch or not. This test
* relies on 2 test XML fixtures that are identical except for the
* 'supported_branches' value:
* - aaa_update_test.1_0-supported.xml
* 'supported_branches' is '8.x-1.,8.x-2.'.
* - aaa_update_test.1_0-unsupported.xml
* 'supported_branches' is '8.x-2.'.
* They both have an '8.x-1.0' release that is unpublished and an '8.x-2.0'
* release that is published and is the expected update.
*/
public function testRevokedRelease(): void {
$this->mockInstalledExtensionsInfo([
'aaa_update_test' => [
'project' => 'aaa_update_test',
'version' => '8.x-1.0',
'hidden' => FALSE,
],
]);
$this->refreshUpdateStatus([
'drupal' => '8.0.0',
$this->updateProject => '1_0-supported',
]);
// @todo Change the version label to 'Recommended version:' in
// https://www.drupal.org/node/3114408.
$this->confirmRevokedStatus('8.x-1.0', '8.x-2.0', 'Also available:');
$this->refreshUpdateStatus([
'drupal' => '8.0.0',
$this->updateProject => '1_0-unsupported',
]);
$this->confirmRevokedStatus('8.x-1.0', '8.x-2.0', 'Recommended version:');
}
/**
* Tests messages when a project release is marked unsupported.
*
* This test confirms unsupported messages are displayed regardless of whether
* the installed version is in a supported branch or not. This test relies on
* 2 test XML fixtures that are identical except for the 'supported_branches'
* value:
* - aaa_update_test.1_0-supported.xml
* 'supported_branches' is '8.x-1.,8.x-2.'.
* - aaa_update_test.1_0-unsupported.xml
* 'supported_branches' is '8.x-2.'.
* They both have an '8.x-1.1' release that has the 'Release type' value of
* 'unsupported' and an '8.x-2.0' release that has the 'Release type' value of
* 'supported' and is the expected update.
*/
public function testUnsupportedRelease(): void {
$this->mockInstalledExtensionsInfo([
'aaa_update_test' => [
'project' => 'aaa_update_test',
'version' => '8.x-1.1',
'hidden' => FALSE,
],
]);
$this->refreshUpdateStatus([
'drupal' => '8.0.0',
$this->updateProject => '1_0-supported',
]);
// @todo Change the version label to 'Recommended version:' in
// https://www.drupal.org/node/3114408.
$this->confirmUnsupportedStatus('8.x-1.1', '8.x-2.0', 'Also available:');
$this->refreshUpdateStatus([
'drupal' => '8.0.0',
$this->updateProject => '1_0-unsupported',
]);
$this->confirmUnsupportedStatus('8.x-1.1', '8.x-2.0', 'Recommended version:');
}
/**
* Tests messages for invalid, empty and missing version strings.
*/
public function testNonStandardVersionStrings(): void {
$version_infos = [
'invalid' => [
'version' => 'llama',
'expected' => 'Invalid version: llama',
],
'empty' => [
'version' => '',
'expected' => 'Empty version',
],
'null' => [
'expected' => 'Invalid version: Unknown',
],
];
foreach ($version_infos as $version_info) {
$installed_extensions = [
'aaa_update_test' => [
'project' => 'aaa_update_test',
'hidden' => FALSE,
],
];
if (isset($version_info['version'])) {
$installed_extensions['aaa_update_test']['version'] = $version_info['version'];
}
$this->mockInstalledExtensionsInfo($installed_extensions);
$this->refreshUpdateStatus([
'drupal' => '8.0.0',
$this->updateProject => '1_0-supported',
]);
$this->standardTests();
$this->assertSession()->elementTextContains('css', $this->updateTableLocator, $version_info['expected']);
}
}
/**
* Asserts that a core compatibility message is correct for an update.
*
* @param string $version
* The version of the update.
* @param string $expected_range
* The expected core compatibility range.
* @param string $expected_release_title
* The expected release title.
* @param bool $is_compatible
* If the update is compatible with the installed version of Drupal.
*
* @internal
*/
protected function assertCoreCompatibilityMessage(string $version, string $expected_range, string $expected_release_title, bool $is_compatible = TRUE): void {
$update_element = $this->findUpdateElementByLabel($expected_release_title);
$this->assertTrue($update_element->hasLink($version));
$compatibility_details = $update_element->find('css', '.project-update__compatibility-details details');
$this->assertStringContainsString("Requires Drupal core: $expected_range", $compatibility_details->getText());
$details_summary_element = $compatibility_details->find('css', 'summary');
if ($is_compatible) {
// If an update is compatible with the installed version of Drupal core,
// the details element should be closed by default.
$this->assertFalse($compatibility_details->hasAttribute('open'));
$this->assertSame('Compatible', $details_summary_element->getText());
}
else {
// If an update is not compatible with the installed version of Drupal
// core, the details element should be open by default.
$this->assertTrue($compatibility_details->hasAttribute('open'));
$this->assertSame('Not compatible', $details_summary_element->getText());
}
$this->assertFalse($update_element->hasLink('Download'));
}
}

View File

@@ -0,0 +1,330 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Functional;
/**
* Tests the Update Manager module's 'Update' form and functionality.
*
* @todo In https://www.drupal.org/project/drupal/issues/3117229 expand this.
*
* @group update
*/
class UpdateManagerUpdateTest extends UpdateTestBase {
use UpdateTestTrait;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'aaa_update_test',
'bbb_update_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$admin_user = $this->drupalCreateUser([
'administer software updates',
'administer site configuration',
]);
$this->drupalLogin($admin_user);
// The installed state of the system is the same for all test cases. What
// varies for each test scenario is which release history fixture we fetch,
// which in turn changes the expected state of the UpdateManagerUpdateForm.
$this->mockInstalledExtensionsInfo([
'aaa_update_test' => [
'project' => 'aaa_update_test',
'version' => '8.x-1.0',
'hidden' => FALSE,
],
'bbb_update_test' => [
'project' => 'bbb_update_test',
'version' => '8.x-1.0',
'hidden' => FALSE,
],
'ccc_update_test' => [
'project' => 'ccc_update_test',
'version' => '8.x-1.0',
'hidden' => FALSE,
],
]);
$this->mockDefaultExtensionsInfo(['version' => '8.0.0']);
}
/**
* Provides data for test scenarios involving incompatible updates.
*
* These test cases rely on the following fixtures containing the following
* releases:
* - aaa_update_test.8.x-1.2.xml
* - 8.x-1.2 Compatible with 8.0.0 core.
* - aaa_update_test.core_compatibility.8.x-1.2_8.x-2.2.xml
* - 8.x-1.2 Requires 8.1.0 and above.
* - bbb_update_test.1_0.xml
* - 8.x-1.0 is the only available release.
* - bbb_update_test.1_1.xml
* - 8.x-1.1 is available and compatible with everything (does not define
* <core_compatibility> at all).
* - bbb_update_test.1_2.xml
* - 8.x-1.1 is available and compatible with everything (does not define
* <core_compatibility> at all).
* - 8.x-1.2 is available and requires Drupal 8.1.0 and above.
*
* @return array[]
* Test data.
*/
public static function incompatibleUpdatesTableProvider() {
return [
'only one compatible' => [
'core_fixture' => '8.1.1',
// aaa_update_test.8.x-1.2.xml has core compatibility set and will test
// the case where $recommended_release['core_compatible'] === TRUE in
// \Drupal\update\Form\UpdateManagerUpdate.
'a_fixture' => '8.x-1.2',
// Use a fixture with only a 8.x-1.0 release so BBB is up to date.
'b_fixture' => '1_0',
'compatible' => [
'AAA' => '8.x-1.2',
],
'incompatible' => [],
],
'only one incompatible' => [
'core_fixture' => '8.1.1',
'a_fixture' => 'core_compatibility.8.x-1.2_8.x-2.2',
// Use a fixture with only a 8.x-1.0 release so BBB is up to date.
'b_fixture' => '1_0',
'compatible' => [],
'incompatible' => [
'AAA' => [
'recommended' => '8.x-1.2',
'range' => '8.1.0 to 8.1.1',
],
],
],
'two compatible, no incompatible' => [
'core_fixture' => '8.1.1',
'a_fixture' => '8.x-1.2',
// bbb_update_test.1_1.xml does not have core compatibility set and will
// test the case where $recommended_release['core_compatible'] === NULL
// in \Drupal\update\Form\UpdateManagerUpdate.
'b_fixture' => '1_1',
'compatible' => [
'AAA' => '8.x-1.2',
'BBB' => '8.x-1.1',
],
'incompatible' => [],
],
'two incompatible, no compatible' => [
'core_fixture' => '8.1.1',
'a_fixture' => 'core_compatibility.8.x-1.2_8.x-2.2',
// bbb_update_test.1_2.xml has core compatibility set and will test the
// case where $recommended_release['core_compatible'] === FALSE in
// \Drupal\update\Form\UpdateManagerUpdate.
'b_fixture' => '1_2',
'compatible' => [],
'incompatible' => [
'AAA' => [
'recommended' => '8.x-1.2',
'range' => '8.1.0 to 8.1.1',
],
'BBB' => [
'recommended' => '8.x-1.2',
'range' => '8.1.0 to 8.1.1',
],
],
],
'one compatible, one incompatible' => [
'core_fixture' => '8.1.1',
'a_fixture' => 'core_compatibility.8.x-1.2_8.x-2.2',
'b_fixture' => '1_1',
'compatible' => [
'BBB' => '8.x-1.1',
],
'incompatible' => [
'AAA' => [
'recommended' => '8.x-1.2',
'range' => '8.1.0 to 8.1.1',
],
],
],
];
}
/**
* Tests the Update form for a single test scenario of incompatible updates.
*
* @dataProvider incompatibleUpdatesTableProvider
*
* @param string $core_fixture
* The fixture file to use for Drupal core.
* @param string $a_fixture
* The fixture file to use for the aaa_update_test module.
* @param string $b_fixture
* The fixture file to use for the bbb_update_test module.
* @param string[] $compatible
* Compatible recommended updates (if any). Keys are module identifier
* ('AAA' or 'BBB') and values are the expected recommended release.
* @param string[][] $incompatible
* Incompatible recommended updates (if any). Keys are module identifier
* ('AAA' or 'BBB') and values are subarrays with the following keys:
* - 'recommended': The recommended version.
* - 'range': The versions of Drupal core required for that version.
*/
public function testIncompatibleUpdatesTable($core_fixture, $a_fixture, $b_fixture, array $compatible, array $incompatible): void {
$assert_session = $this->assertSession();
$compatible_table_locator = '[data-drupal-selector="edit-projects"]';
$incompatible_table_locator = '[data-drupal-selector="edit-not-compatible"]';
$this->refreshUpdateStatus(['drupal' => $core_fixture, 'aaa_update_test' => $a_fixture, 'bbb_update_test' => $b_fixture]);
$this->drupalGet('admin/reports/updates/update');
if ($compatible) {
// Verify the number of rows in the table.
$assert_session->elementsCount('css', "$compatible_table_locator tbody tr", count($compatible));
// We never want to see a compatibility range in the compatible table.
$assert_session->elementTextNotContains('css', $compatible_table_locator, 'Requires Drupal core');
foreach ($compatible as $module => $version) {
$compatible_row = "$compatible_table_locator tbody tr:contains('$module Update test')";
// First <td> is the checkbox, so start with td #2.
$assert_session->elementTextContains('css', "$compatible_row td:nth-of-type(2)", "$module Update test");
// Both contrib modules use 8.x-1.0 as the currently installed version.
$assert_session->elementTextContains('css', "$compatible_row td:nth-of-type(3)", '8.x-1.0');
$assert_session->elementTextContains('css', "$compatible_row td:nth-of-type(4)", $version);
}
}
else {
// Verify there is no compatible updates table.
$assert_session->elementNotExists('css', $compatible_table_locator);
}
if ($incompatible) {
// Verify the number of rows in the table.
$assert_session->elementsCount('css', "$incompatible_table_locator tbody tr", count($incompatible));
foreach ($incompatible as $module => $data) {
$incompatible_row = "$incompatible_table_locator tbody tr:contains('$module Update test')";
$assert_session->elementTextContains('css', "$incompatible_row td:nth-of-type(1)", "$module Update test");
// Both contrib modules use 8.x-1.0 as the currently installed version.
$assert_session->elementTextContains('css', "$incompatible_row td:nth-of-type(2)", '8.x-1.0');
$assert_session->elementTextContains('css', "$incompatible_row td:nth-of-type(3)", $data['recommended']);
$assert_session->elementTextContains('css', "$incompatible_row td:nth-of-type(3)", 'Requires Drupal core: ' . $data['range']);
}
}
else {
// Verify there is no incompatible updates table.
$assert_session->elementNotExists('css', $incompatible_table_locator);
}
}
/**
* Tests the Update form with an uninstalled module in the system.
*/
public function testUninstalledUpdatesTable(): void {
$assert_session = $this->assertSession();
$compatible_table_locator = '[data-drupal-selector="edit-projects"]';
$uninstalled_table_locator = '[data-drupal-selector="edit-uninstalled-projects"]';
$fixtures = [
'drupal' => '8.1.1',
'aaa_update_test' => '8.x-1.2',
// Use a fixture with only a 8.x-1.0 release so BBB is up to date.
'bbb_update_test' => '1_0',
// CCC is not installed and is missing an update, 8.x-1.1.
'ccc_update_test' => '1_1',
];
$this->refreshUpdateStatus($fixtures);
$this->drupalGet('admin/reports/updates/update');
// Confirm there is no table for uninstalled extensions.
$assert_session->pageTextNotContains('CCC Update test');
$assert_session->responseNotContains('<h2>Uninstalled</h2>');
// Confirm the table for installed modules exists without a header.
$assert_session->responseNotContains('<h2>Installed</h2>');
$assert_session->elementNotExists('css', $uninstalled_table_locator);
$assert_session->elementsCount('css', "$compatible_table_locator tbody tr", 1);
$compatible_headers = [
// First column has no header, it's the select-all checkbox.
'th:nth-of-type(2)' => 'Name',
'th:nth-of-type(3)' => 'Site version',
'th:nth-of-type(4)' => 'Recommended version',
];
$this->checkTableHeaders($compatible_table_locator, $compatible_headers);
$installed_row = "$compatible_table_locator tbody tr";
$assert_session->elementsCount('css', $installed_row, 1);
$assert_session->elementTextContains('css', "$compatible_table_locator td:nth-of-type(2)", "AAA Update test");
$assert_session->elementTextContains('css', "$compatible_table_locator td:nth-of-type(3)", '8.x-1.0');
$assert_session->elementTextContains('css', "$compatible_table_locator td:nth-of-type(4)", '8.x-1.2');
// Change the setting so we check for uninstalled modules, too.
$this->config('update.settings')
->set('check.disabled_extensions', TRUE)
->save();
// Reload the page so the new setting goes into effect.
$this->drupalGet('admin/reports/updates/update');
// Confirm the table for installed modules exists with a header.
$assert_session->responseContains('<h2>Installed</h2>');
$assert_session->elementsCount('css', "$compatible_table_locator tbody tr", 1);
$this->checkTableHeaders($compatible_table_locator, $compatible_headers);
// Confirm the table for uninstalled extensions exists.
$assert_session->responseContains('<h2>Uninstalled</h2>');
$uninstalled_headers = [
// First column has no header, it's the select-all checkbox.
'th:nth-of-type(2)' => 'Name',
'th:nth-of-type(3)' => 'Site version',
'th:nth-of-type(4)' => 'Recommended version',
];
$this->checkTableHeaders($uninstalled_table_locator, $uninstalled_headers);
$uninstalled_row = "$uninstalled_table_locator tbody tr";
$assert_session->elementsCount('css', $uninstalled_row, 1);
$assert_session->elementTextContains('css', "$uninstalled_row td:nth-of-type(2)", "CCC Update test");
$assert_session->elementTextContains('css', "$uninstalled_row td:nth-of-type(3)", '8.x-1.0');
$assert_session->elementTextContains('css', "$uninstalled_row td:nth-of-type(4)", '8.x-1.1');
}
/**
* Checks headers for a given table on the Update form.
*
* @param string $table_locator
* CSS locator to find the table to check the headers on.
* @param string[] $expected_headers
* Array of expected header texts, keyed by CSS selectors relative to the
* thead tr (for example, "th:nth-of-type(3)").
*/
private function checkTableHeaders($table_locator, array $expected_headers) {
$assert_session = $this->assertSession();
$assert_session->elementExists('css', $table_locator);
foreach ($expected_headers as $locator => $header) {
$assert_session->elementTextContains('css', "$table_locator thead tr $locator", $header);
}
}
/**
* Tests the deprecation warnings.
*
* @group legacy
*/
public function testDeprecationWarning(): void {
$this->drupalGet('admin/theme/update');
$this->expectDeprecation('The path /admin/theme/update is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use /admin/appearance/update. See https://www.drupal.org/node/3382805');
$this->assertSession()->statusMessageContains("You have been redirected from admin/theme/update. Update links, shortcuts, and bookmarks to use admin/appearance/update.", 'warning');
}
}

View File

@@ -0,0 +1,259 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Functional;
use Drupal\Core\Url;
use Drupal\Tests\Traits\Core\CronRunTrait;
/**
* Tests general functionality of the Update module.
*
* @group update
*/
class UpdateMiscTest extends UpdateTestBase {
use CronRunTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['language', 'block'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$setting = [
'#all' => [
'version' => '8.0.0',
],
];
$this->config('update_test.settings')->set('system_info', $setting)->save();
$this->drupalPlaceBlock('local_actions_block');
}
/**
* Ensures that the local actions appear.
*/
public function testLocalActions(): void {
$admin_user = $this->drupalCreateUser([
'administer site configuration',
'administer modules',
'administer software updates',
'administer themes',
]);
$this->drupalLogin($admin_user);
$this->drupalGet('admin/modules');
$this->clickLink('Add new module');
$this->assertSession()->addressEquals('admin/modules/install');
$this->drupalGet('admin/appearance');
$this->clickLink('Add new theme');
$this->assertSession()->addressEquals('admin/theme/install');
$this->drupalGet('admin/reports/updates');
$this->clickLink('Add new module or theme');
$this->assertSession()->addressEquals('admin/reports/updates/install');
}
/**
* Checks that clearing the disk cache works.
*/
public function testClearDiskCache(): void {
$directories = [
_update_manager_cache_directory(FALSE),
_update_manager_extract_directory(FALSE),
];
// Check that update directories does not exists.
foreach ($directories as $directory) {
$this->assertDirectoryDoesNotExist($directory);
}
// Method must not fail if update directories do not exists.
update_clear_update_disk_cache();
}
/**
* Tests the Update Manager module when the update server returns 503 errors.
*/
public function testServiceUnavailable(): void {
$admin_user = $this->drupalCreateUser([
'administer site configuration',
]);
$this->drupalLogin($admin_user);
$this->refreshUpdateStatus([], '503-error');
// Ensure that no "Warning: SimpleXMLElement..." parse errors are found.
$this->assertSession()->pageTextNotContains('SimpleXMLElement');
$this->assertSession()->pageTextContainsOnce('Failed to get available update data for one project.');
}
/**
* Tests that exactly one fetch task per project is created and not more.
*/
public function testFetchTasks(): void {
$project_a = [
'name' => 'aaa_update_test',
];
$project_b = [
'name' => 'bbb_update_test',
];
$queue = \Drupal::queue('update_fetch_tasks');
$this->assertEquals(0, $queue->numberOfItems(), 'Queue is empty');
update_create_fetch_task($project_a);
$this->assertEquals(1, $queue->numberOfItems(), 'Queue contains one item');
update_create_fetch_task($project_b);
$this->assertEquals(2, $queue->numberOfItems(), 'Queue contains two items');
// Try to add a project again.
update_create_fetch_task($project_a);
$this->assertEquals(2, $queue->numberOfItems(), 'Queue still contains two items');
// Clear storage and try again.
update_storage_clear();
update_create_fetch_task($project_a);
$this->assertEquals(2, $queue->numberOfItems(), 'Queue contains two items');
}
/**
* Checks the messages at admin/modules when the site is up to date.
*/
public function testModulePageUpToDate(): void {
$this->drupalLogin($this->drupalCreateUser([
'administer site configuration',
'view update notifications',
]));
// Instead of using refreshUpdateStatus(), set these manually.
$this->config('update.settings')
->set('fetch.url', Url::fromRoute('update_test.update_test')
->setAbsolute()
->toString())
->save();
$this->config('update_test.settings')
->set('xml_map', ['drupal' => '8.0.0'])
->save();
$this->drupalGet('admin/reports/updates');
$this->clickLink('Check manually');
$this->checkForMetaRefresh();
$this->assertSession()->pageTextContains('Checked available update data for one project.');
$this->drupalGet('admin/modules');
$this->assertSession()->pageTextNotContains('There are updates available for your version of Drupal.');
$this->assertSession()->pageTextNotContains('There is a security update available for your version of Drupal.');
}
/**
* Checks the messages at admin/modules when an update is missing.
*/
public function testModulePageRegularUpdate(): void {
$this->drupalLogin($this->drupalCreateUser([
'administer site configuration',
'administer modules',
'view update notifications',
]));
// Instead of using refreshUpdateStatus(), set these manually.
$this->config('update.settings')
->set('fetch.url', Url::fromRoute('update_test.update_test')->setAbsolute()->toString())
->save();
$this->config('update_test.settings')
->set('xml_map', ['drupal' => '8.0.1'])
->save();
$this->drupalGet('admin/reports/updates');
$this->clickLink('Check manually');
$this->checkForMetaRefresh();
$this->assertSession()->pageTextContains('Checked available update data for one project.');
$this->drupalGet('admin/modules');
$this->assertSession()->pageTextContains('There are updates available for your version of Drupal.');
$this->assertSession()->pageTextNotContains('There is a security update available for your version of Drupal.');
// A user without the "view update notifications" permission shouldn't be
// notified about available updates.
$this->drupalLogin($this->drupalCreateUser([
'administer site configuration',
'administer modules',
]));
$this->drupalGet('admin/modules');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextNotContains('There are updates available for your version of Drupal.');
}
/**
* Checks the messages at admin/modules when a security update is missing.
*/
public function testModulePageSecurityUpdate(): void {
$this->drupalLogin($this->drupalCreateUser([
'administer site configuration',
'administer modules',
'administer themes',
'view update notifications',
]));
// Instead of using refreshUpdateStatus(), set these manually.
$this->config('update.settings')
->set('fetch.url', Url::fromRoute('update_test.update_test')->setAbsolute()->toString())
->save();
$this->mockReleaseHistory(['drupal' => 'sec.8.0.2']);
$this->drupalGet('admin/reports/updates');
$this->clickLink('Check manually');
$this->checkForMetaRefresh();
$this->assertSession()->pageTextContains('Checked available update data for one project.');
$this->drupalGet('admin/modules');
$this->assertSession()->pageTextNotContains('There are updates available for your version of Drupal.');
$this->assertSession()->pageTextContains('There is a security update available for your version of Drupal.');
// Make sure admin/appearance warns you you're missing a security update.
$this->drupalGet('admin/appearance');
$this->assertSession()->pageTextNotContains('There are updates available for your version of Drupal.');
$this->assertSession()->pageTextContains('There is a security update available for your version of Drupal.');
// Make sure duplicate messages don't appear on Update status pages.
$this->drupalGet('admin/reports/status');
$this->assertSession()->pageTextContainsOnce('There is a security update available for your version of Drupal.');
$this->drupalGet('admin/reports/updates');
$this->assertSession()->pageTextNotContains('There is a security update available for your version of Drupal.');
$this->drupalGet('admin/reports/updates/settings');
$this->assertSession()->pageTextNotContains('There is a security update available for your version of Drupal.');
}
/**
* Checks that running cron updates the list of available updates.
*/
public function testModulePageRunCron(): void {
$this->config('update.settings')
->set('fetch.url', Url::fromRoute('update_test.update_test')->setAbsolute()->toString())
->save();
$this->mockReleaseHistory(['drupal' => '8.0.0']);
$this->cronRun();
$this->drupalGet('admin/modules');
$this->assertSession()->pageTextNotContains('No update information available.');
}
/**
* Checks language module in core package at admin/reports/updates.
*/
public function testLanguageModuleUpdate(): void {
$this->drupalLogin($this->drupalCreateUser([
'administer site configuration',
]));
// Instead of using refreshUpdateStatus(), set these manually.
$this->config('update.settings')
->set('fetch.url', Url::fromRoute('update_test.update_test')->setAbsolute()->toString())
->save();
$this->mockReleaseHistory(['drupal' => '0.1']);
$this->drupalGet('admin/reports/updates');
$this->assertSession()->pageTextContains('Language');
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Functional;
/**
* Tests the Update Manager module with a contrib module with semver versions.
*
* @group update
* @group #slow
*/
class UpdateSemverContribBaselineTest extends UpdateSemverContribTestBase {
use UpdateSemverTestBaselineTrait;
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Functional;
/**
* Tests Update Manager with a security update available for a contrib project.
*
* @group update
* @group #slow
*/
class UpdateSemverContribSecurityAvailabilityTest extends UpdateSemverContribTestBase {
use UpdateSemverTestSecurityAvailabilityTrait;
}

View File

@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Functional;
/**
* Base class for Update manager semantic versioning tests of contrib projects.
*
* This wires up the protected data from UpdateSemverTestBase for a contrib
* module with semantic version releases.
*/
class UpdateSemverContribTestBase extends UpdateSemverTestBase {
/**
* {@inheritdoc}
*/
protected $updateTableLocator = 'table.update:nth-of-type(2)';
/**
* {@inheritdoc}
*/
protected $updateProject = 'semver_test';
/**
* {@inheritdoc}
*/
protected $projectTitle = 'Semver Test';
/**
* {@inheritdoc}
*/
protected static $modules = ['semver_test'];
/**
* {@inheritdoc}
*/
protected function setProjectInstalledVersion($version) {
$this->mockInstalledExtensionsInfo([
$this->updateProject => [
'project' => $this->updateProject,
'version' => $version,
'hidden' => FALSE,
],
// Ensure Drupal core on the same version for all test runs.
'drupal' => [
'project' => 'drupal',
'version' => '8.0.0',
'hidden' => FALSE,
],
]);
$this->mockDefaultExtensionsInfo(['version' => '8.0.0']);
}
/**
* Tests updates from legacy versions to the semver versions.
*/
public function testUpdatesLegacyToSemver(): void {
// Test cases where the legacy branch is in the XML 'supported_branches' and
// when it is not.
foreach ([TRUE, FALSE] as $legacy_supported) {
// Test 2 legacy majors.
foreach ([7, 8] as $legacy_major) {
$semver_major = $legacy_major + 1;
$installed_versions = [
"8.x-$legacy_major.0-alpha1",
"8.x-$legacy_major.0-beta1",
"8.x-$legacy_major.0",
"8.x-$legacy_major.1-alpha1",
"8.x-$legacy_major.1-beta1",
"8.x-$legacy_major.1",
];
foreach ($installed_versions as $installed_version) {
$this->setProjectInstalledVersion($installed_version);
if ($legacy_supported) {
$fixture = $legacy_major === 7 ? '8.1.0' : '9.1.0';
}
else {
if ($legacy_major === 8) {
continue;
}
$fixture = '8.1.0-legacy-unsupported';
}
$this->refreshUpdateStatus([$this->updateProject => $fixture]);
$this->assertUpdateTableTextNotContains('Security update required!');
$this->assertSession()->elementTextContains('css', $this->updateTableLocator . " .project-update__title", $installed_version);
if ($legacy_supported) {
// All installed versions should indicate that there is an update
// available for the next major version of the module.
// '$legacy_major.1.0' is shown for the next major version because
// it is the latest full release for that major.
// @todo Determine if both 8.0.0 and 8.0.1 should be expected as
// "Also available" releases in
// https://www.drupal.org/project/node/3100115.
$this->assertVersionUpdateLinks('Also available:', "$semver_major.1.0");
if ($installed_version === "8.x-$legacy_major.1") {
$this->assertUpdateTableTextContains('Up to date');
$this->assertUpdateTableTextNotContains('Update available');
}
else {
$this->assertUpdateTableTextNotContains('Up to date');
$this->assertUpdateTableTextContains('Update available');
// All installed versions besides 8.x-$legacy_major.1 should
// recommend 8.x-$legacy_major.1 because it is the latest full
// release for the major.
$this->assertVersionUpdateLinks('Recommended version:', "8.x-$legacy_major.1");
}
}
else {
// If '8.x-7.' is not in the XML 'supported_branches' value then the
// latest release for the next major should always be recommended.
$this->assertVersionUpdateLinks('Recommended version:', "$semver_major.1.0");
}
}
}
}
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Functional;
/**
* Tests semantic version handling in the Update Manager for Drupal core.
*
* @group update
* @group #slow
*/
class UpdateSemverCoreBaselineTest extends UpdateSemverCoreTestBase {
use UpdateSemverTestBaselineTrait;
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Functional;
/**
* Tests Update Manager with a security update available for Drupal core.
*
* @group update
* @group #slow
*/
class UpdateSemverCoreSecurityAvailabilityTest extends UpdateSemverCoreTestBase {
use UpdateSemverTestSecurityAvailabilityTrait;
}

View File

@@ -0,0 +1,246 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Functional;
/**
* Tests the security coverage messages for Drupal core versions.
*
* @group update
* @group #slow
*/
class UpdateSemverCoreSecurityCoverageTest extends UpdateSemverCoreTestBase {
/**
* Tests the security coverage messages for Drupal core versions.
*
* @param string $installed_version
* The installed Drupal version to test.
* @param string $fixture
* The test fixture that contains the test XML.
* @param string $requirements_section_heading
* The requirements section heading.
* @param string $message
* The expected coverage message.
* @param string $mock_date
* The mock date to use if needed in the format CCYY-MM-DD. If an empty
* string is provided, no mock date will be used.
*
* @dataProvider securityCoverageMessageProvider
*/
public function testSecurityCoverageMessage($installed_version, $fixture, $requirements_section_heading, $message, $mock_date): void {
\Drupal::state()->set('update_test.mock_date', $mock_date);
$this->setProjectInstalledVersion($installed_version);
$this->refreshUpdateStatus(['drupal' => $fixture]);
$this->drupalGet('admin/reports/status');
if (empty($requirements_section_heading)) {
$this->assertSession()->pageTextNotContains('Drupal core security coverage');
return;
}
$all_requirements_details = $this->getSession()->getPage()->findAll(
'css',
'details.system-status-report__entry:contains("Drupal core security coverage")'
);
// Ensure we only have 1 security message section.
$this->assertCount(1, $all_requirements_details);
$requirements_details = $all_requirements_details[0];
// Ensure that messages are under the correct heading which could be
// 'Checked', 'Warnings found', or 'Errors found'.
$requirements_section_element = $requirements_details->getParent();
$this->assertCount(1, $requirements_section_element->findAll('css', "h3:contains('$requirements_section_heading')"));
$actual_message = $requirements_details->find('css', 'div.system-status-report__entry__value')->getText();
$this->assertNotEmpty($actual_message);
$this->assertEquals($message, $actual_message);
}
/**
* Data provider for testSecurityCoverageMessage().
*
* These test cases rely on the following fixtures containing the following
* releases:
* - drupal.sec.8.2.0_3.0-rc1.xml
* - 8.2.0
* - 8.3.0-rc1
* - drupal.sec.8.2.0.xml
* - 8.2.0
* - drupal.sec.8.2.0_9.0.0.xml
* - 8.2.0
* - 9.0.0
* - drupal.sec.9.5.0.xml
* - 9.4.0
* - 9.5.0
* - drupal.sec.10.5.0.xml
* - 10.4.0
* - 10.5.0
*/
public static function securityCoverageMessageProvider() {
$release_coverage_message = 'Visit the release cycle overview for more information on supported releases.';
$coverage_ended_message = 'Coverage has ended';
$update_asap_message = 'Update to a supported minor as soon as possible to continue receiving security updates.';
$update_soon_message = 'Update to a supported minor version soon to continue receiving security updates.';
$test_cases = [
'8.0.0, unsupported' => [
'installed_version' => '8.0.0',
'fixture' => 'sec.8.2.0_8.3.0-rc1',
'requirements_section_heading' => 'Errors found',
'message' => "$coverage_ended_message $update_asap_message $release_coverage_message",
'mock_date' => '',
],
'8.1.0, supported with 3rc' => [
'installed_version' => '8.1.0',
'fixture' => 'sec.8.2.0_8.3.0-rc1',
'requirements_section_heading' => 'Warnings found',
'message' => "Covered until 8.3.0 Update to 8.2 or higher soon to continue receiving security updates. $release_coverage_message",
'mock_date' => '',
],
'8.1.0, supported' => [
'installed_version' => '8.1.0',
'fixture' => 'sec.8.2.0',
'requirements_section_heading' => 'Warnings found',
'message' => "Covered until 8.3.0 Update to 8.2 or higher soon to continue receiving security updates. $release_coverage_message",
'mock_date' => '',
],
'8.2.0, supported with 3rc' => [
'installed_version' => '8.2.0',
'fixture' => 'sec.8.2.0_8.3.0-rc1',
'requirements_section_heading' => 'Checked',
'message' => "Covered until 8.4.0 $release_coverage_message",
'mock_date' => '',
],
'8.2.0, supported' => [
'installed_version' => '8.2.0',
'fixture' => 'sec.8.2.0',
'requirements_section_heading' => 'Checked',
'message' => "Covered until 8.4.0 $release_coverage_message",
'mock_date' => '',
],
// Ensure we don't show messages for pre-release or dev versions.
'8.2.0-beta2, no message' => [
'installed_version' => '8.2.0-beta2',
'fixture' => 'sec.8.2.0_8.3.0-rc1',
'requirements_section_heading' => '',
'message' => '',
'mock_date' => '',
],
'8.1.0-dev, no message' => [
'installed_version' => '8.1.0-dev',
'fixture' => 'sec.8.2.0_8.3.0-rc1',
'requirements_section_heading' => '',
'message' => '',
'mock_date' => '',
],
// Ensures the message is correct if the next major version has been
// released and the additional minors indicated by
// CORE_MINORS_WITH_SECURITY_COVERAGE minors have been released.
'8.0.0, 9 unsupported' => [
'installed_version' => '8.0.0',
'fixture' => 'sec.8.2.0_9.0.0',
'requirements_section_heading' => 'Errors found',
'message' => "$coverage_ended_message $update_asap_message $release_coverage_message",
'mock_date' => '',
],
// Ensures the message is correct if the next major version has been
// released and the additional minors indicated by
// CORE_MINORS_WITH_SECURITY_COVERAGE minors have not been released.
'8.2.0, 9 warning' => [
'installed_version' => '8.2.0',
'fixture' => 'sec.8.2.0_9.0.0',
'requirements_section_heading' => 'Warnings found',
'message' => "Covered until 8.4.0 Update to 8.3 or higher soon to continue receiving security updates. $release_coverage_message",
'mock_date' => '',
],
];
// Drupal 9.4.x test cases.
$test_cases += [
// Ensure that a message is displayed during 9.4's active support.
'9.4.0, supported' => [
'installed_version' => '9.4.0',
'fixture' => 'sec.9.5.0',
'requirements_section_heading' => 'Checked',
'message' => "Covered until 2023-Jun-21 $release_coverage_message",
'mock_date' => '2022-12-13',
],
// Ensure a warning is displayed if less than six months remain until the
// end of 9.4's security coverage.
'9.4.0, supported, 6 months warn' => [
'installed_version' => '9.4.0',
'fixture' => 'sec.9.5.0',
'requirements_section_heading' => 'Warnings found',
'message' => "Covered until 2023-Jun-21 $update_soon_message $release_coverage_message",
'mock_date' => '2022-12-14',
],
];
// Ensure that the message does not change, including on the last day of
// security coverage.
$test_cases['9.4.0, supported, last day warn'] = $test_cases['9.4.0, supported, 6 months warn'];
$test_cases['9.4.0, supported, last day warn']['mock_date'] = '2023-06-20';
// Ensure that if the 9.4 support window is finished a message is
// displayed.
$test_cases['9.4.0, support over'] = [
'installed_version' => '9.4.0',
'fixture' => 'sec.9.5.0',
'requirements_section_heading' => 'Errors found',
'message' => "$coverage_ended_message $update_asap_message $release_coverage_message",
'mock_date' => '2023-06-22',
];
// Drupal 9.5 test cases.
$test_cases['9.5.0, supported'] = [
'installed_version' => '9.5.0',
'fixture' => 'sec.9.5.0',
'requirements_section_heading' => 'Checked',
'message' => "Covered until 2023-Nov $release_coverage_message",
'mock_date' => '2023-01-01',
];
// Ensure a warning is displayed if less than six months remain until the
// end of 9.5's security coverage.
$test_cases['9.5.0, supported, 6 months warn'] = [
'installed_version' => '9.5.0',
'fixture' => 'sec.9.5.0',
'requirements_section_heading' => 'Warnings found',
'message' => "Covered until 2023-Nov $update_soon_message $release_coverage_message",
'mock_date' => '2023-05-15',
];
// Ensure that the message does not change, including on the last day of
// security coverage.
$test_cases['9.5.0, supported, last day warn'] = $test_cases['9.5.0, supported, 6 months warn'];
$test_cases['9.5.0, supported, last day warn']['mock_date'] = '2023-10-31';
// Ensure that if the support window is finished a message is displayed.
$test_cases['9.5.0, support over'] = [
'installed_version' => '9.5.0',
'fixture' => 'sec.9.5.0',
'requirements_section_heading' => 'Errors found',
'message' => "$coverage_ended_message $update_asap_message $release_coverage_message",
'mock_date' => '2023-11-01',
];
// Drupal 9 test cases.
$test_cases += [
// Ensure the end dates for 9.4 and 9.5 only apply to major version 9.
'10.5.0' => [
'installed_version' => '10.5.0',
'fixture' => 'sec.10.5.0',
'requirements_section_heading' => 'Checked',
'message' => "Covered until 10.7.0 $release_coverage_message",
'mock_date' => '',
],
'10.4.0' => [
'installed_version' => '10.4.0',
'fixture' => 'sec.10.5.0',
'requirements_section_heading' => 'Warnings found',
'message' => "Covered until 10.6.0 Update to 10.5 or higher soon to continue receiving security updates. $release_coverage_message",
'mock_date' => '',
],
];
return $test_cases;
}
}

View File

@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Functional;
use Drupal\Core\Url;
/**
* Tests edge cases of the Available Updates report UI.
*
* For example, manually checking for updates, recovering from problems
* connecting to the release history server, clearing the disk cache, and more.
*
* @group update
* @group #slow
*/
class UpdateSemverCoreTest extends UpdateSemverCoreTestBase {
/**
* Ensures proper results where there are date mismatches among modules.
*/
public function testDatestampMismatch(): void {
$this->mockInstalledExtensionsInfo([
'block' => [
// This is 2001-09-09 01:46:40 GMT, so test for "2001-Sep-".
'datestamp' => '1000000000',
],
]);
// We need to think we're running a -dev snapshot to see dates.
$this->mockDefaultExtensionsInfo([
'version' => '8.1.0-dev',
'datestamp' => time(),
]);
$this->refreshUpdateStatus(['drupal' => 'dev']);
$this->assertSession()->pageTextNotContains('2001-Sep-');
$this->assertSession()->pageTextContains('Up to date');
$this->assertSession()->pageTextNotContains('Update available');
$this->assertSession()->pageTextNotContains('Security update required!');
}
/**
* Tests the Update Manager module when the update server returns 503 errors.
*/
public function testServiceUnavailable(): void {
$this->refreshUpdateStatus([], '503-error');
// Ensure that no "Warning: SimpleXMLElement..." parse errors are found.
$this->assertSession()->pageTextNotContains('SimpleXMLElement');
$this->assertSession()->pageTextContainsOnce('Failed to get available update data for one project.');
}
/**
* Tests that exactly one fetch task per project is created and not more.
*/
public function testFetchTasks(): void {
$project_a = [
'name' => 'aaa_update_test',
];
$project_b = [
'name' => 'bbb_update_test',
];
$queue = \Drupal::queue('update_fetch_tasks');
$this->assertEquals(0, $queue->numberOfItems(), 'Queue is empty');
update_create_fetch_task($project_a);
$this->assertEquals(1, $queue->numberOfItems(), 'Queue contains one item');
update_create_fetch_task($project_b);
$this->assertEquals(2, $queue->numberOfItems(), 'Queue contains two items');
// Try to add a project again.
update_create_fetch_task($project_a);
$this->assertEquals(2, $queue->numberOfItems(), 'Queue still contains two items');
// Clear storage and try again.
update_storage_clear();
update_create_fetch_task($project_a);
$this->assertEquals(2, $queue->numberOfItems(), 'Queue contains two items');
}
/**
* Checks that Drupal recovers after problems connecting to update server.
*
* This test uses the following XML fixtures.
* - drupal.broken.xml
* - drupal.sec.8.0.2.xml
* 'supported_branches' is '8.0.,8.1.'.
*/
public function testBrokenThenFixedUpdates(): void {
$this->drupalLogin($this->drupalCreateUser([
'administer site configuration',
'view update notifications',
'access administration pages',
]));
$this->setProjectInstalledVersion('8.0.0');
// Instead of using refreshUpdateStatus(), set these manually.
$this->config('update.settings')
->set('fetch.url', Url::fromRoute('update_test.update_test')->setAbsolute()->toString())
->save();
// Use update XML that has no information to simulate a broken response from
// the update server.
$this->mockReleaseHistory(['drupal' => 'broken']);
// This will retrieve broken updates.
$this->cronRun();
$this->drupalGet('admin/reports/status');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('There was a problem checking available updates for Drupal.');
$this->mockReleaseHistory(['drupal' => 'sec.8.0.2']);
// Simulate the update_available_releases state expiring before cron is run
// and the state is used by \Drupal\update\UpdateManager::getProjects().
\Drupal::keyValueExpirable('update_available_releases')->deleteAll();
// This cron run should retrieve fixed updates.
$this->cronRun();
$this->drupalGet('admin/config');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('There is a security update available for your version of Drupal.');
}
/**
* Tests when a dev release does not have a date.
*/
public function testDevNoReleaseDate(): void {
$this->setProjectInstalledVersion('8.0.x-dev');
$this->refreshUpdateStatus([$this->updateProject => 'dev-no-date']);
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Functional;
/**
* Base class for Update manager semantic versioning tests of Drupal core.
*
* This wires up the protected data from UpdateSemverTestBase for Drupal core
* with semantic version releases.
*/
class UpdateSemverCoreTestBase extends UpdateSemverTestBase {
/**
* {@inheritdoc}
*/
protected $updateTableLocator = 'table.update';
/**
* {@inheritdoc}
*/
protected $updateProject = 'drupal';
/**
* {@inheritdoc}
*/
protected $projectTitle = 'Drupal';
/**
* {@inheritdoc}
*/
protected function setProjectInstalledVersion($version) {
$this->mockDefaultExtensionsInfo(['version' => $version]);
}
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Functional;
use Drupal\Tests\Traits\Core\CronRunTrait;
/**
* Common setup and utility methods to test projects that use semver releases.
*
* For classes that extend this class, the XML fixtures they use will start with
* ::$projectTitle.
*
* @group update
*/
abstract class UpdateSemverTestBase extends UpdateTestBase {
use CronRunTrait;
use UpdateTestTrait;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['language', 'block'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The title of the project being tested.
*
* @var string
*/
protected $projectTitle;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$admin_user = $this->drupalCreateUser([
'administer site configuration',
'view update notifications',
]);
$this->drupalLogin($admin_user);
$this->drupalPlaceBlock('local_actions_block');
}
/**
* {@inheritdoc}
*/
protected function refreshUpdateStatus($xml_map, $url = 'update-test') {
if (!isset($xml_map['drupal'])) {
$xml_map['drupal'] = '8.0.0';
}
parent::refreshUpdateStatus($xml_map, $url);
}
/**
* Sets the project installed version.
*
* @param string $version
* The version number.
*/
abstract protected function setProjectInstalledVersion($version);
}

View File

@@ -0,0 +1,244 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Functional;
use Drupal\Core\Link;
use Drupal\Core\Url;
/**
* Provides test methods for semver tests shared between core and contrib.
*
* All of this "baseline" semver behavior should be the same for both Drupal
* core and contributed projects that use semantic versioning.
*/
trait UpdateSemverTestBaselineTrait {
/**
* Tests the Update Manager module when no updates are available.
*
* The XML fixture file 'drupal.8.1.0.xml' which is one of the XML files this
* test uses also contains 2 extra releases that are newer than '8.0.1'. These
* releases will not show as available updates because of the following
* reasons:
* - '8.0.2' is an unpublished release.
* - '8.0.3' is marked as 'Release type' 'Unsupported'.
*/
public function testNoUpdatesAvailable(): void {
foreach ([0, 1] as $minor_version) {
foreach ([0, 1] as $patch_version) {
foreach (['-alpha1', '-beta1', ''] as $extra_version) {
$this->setProjectInstalledVersion("8.$minor_version.$patch_version" . $extra_version);
$this->refreshUpdateStatus([$this->updateProject => "8.$minor_version.$patch_version" . $extra_version]);
$this->standardTests();
// The XML test fixtures for this method all contain the '8.2.0'
// release but because '8.2.0' is not in a supported branch it will
// not be in the available updates.
$this->assertUpdateTableElementNotContains('8.2.0');
$this->assertUpdateTableTextContains('Up to date');
$this->assertUpdateTableTextNotContains('Update available');
$this->assertUpdateTableTextNotContains('Security update required!');
$this->assertUpdateTableElementContains('check.svg');
}
}
}
}
/**
* Tests the Update Manager module when one normal update is available.
*/
public function testNormalUpdateAvailable(): void {
$this->setProjectInstalledVersion('8.0.0');
// Ensure that the update check requires a token.
$this->drupalGet('admin/reports/updates/check');
$this->assertSession()->statusCodeEquals(403);
foreach ([0, 1] as $minor_version) {
foreach (['-alpha1', '-beta1', ''] as $extra_version) {
$full_version = "8.$minor_version.1$extra_version";
$this->refreshUpdateStatus([$this->updateProject => "8.$minor_version.1" . $extra_version]);
$this->standardTests();
$this->assertUpdateTableTextNotContains('Security update required!');
// The XML test fixtures for this method all contain the '8.2.0' release
// but because '8.2.0' is not in a supported branch it will not be in
// the available updates.
$this->assertSession()->responseNotContains('8.2.0');
switch ($minor_version) {
case 0:
// Both stable and unstable releases are available.
// A stable release is the latest.
if ($extra_version == '') {
$this->assertNoExtraVersion($full_version);
}
// Only unstable releases are available.
// An unstable release is the latest.
else {
$this->assertUpdateTableTextContains('Up to date');
$this->assertUpdateTableTextNotContains('Update available');
$this->assertUpdateTableTextNotContains('Recommended version:');
$this->assertVersionUpdateLinks('Latest version:', $full_version);
$this->assertUpdateTableElementContains('check.svg');
}
break;
case 1:
// Both stable and unstable releases are available.
// A stable release is the latest.
if ($extra_version == '') {
$this->assertNoExtraVersion($full_version);
}
// Both stable and unstable releases are available.
// An unstable release is the latest.
else {
$this->assertUpdateTableTextNotContains('Up to date');
$this->assertUpdateTableTextContains('Update available');
$this->assertVersionUpdateLinks('Recommended version:', '8.1.0');
$this->assertVersionUpdateLinks('Latest version:', $full_version);
$this->assertUpdateTableElementContains('warning.svg');
}
break;
}
}
}
}
/**
* Asserts update table when there is no extra version.
*
* @param string $full_version
* The recommended version.
*
* @return void
*/
protected function assertNoExtraVersion(string $full_version): void {
$this->assertUpdateTableTextNotContains('Up to date');
$this->assertUpdateTableTextContains('Update available');
$this->assertVersionUpdateLinks('Recommended version:', $full_version);
$this->assertUpdateTableTextNotContains('Latest version:');
$this->assertUpdateTableElementContains('warning.svg');
}
/**
* Tests the Update Manager module when major updates are available.
*
* This includes testing when the next major is available as well as when both
* the current major version and the next major version are supported. There
* are two release history files to support this.
* - drupal.9.xml and semver_test.9.xml: These declare one major release
* supported, 9.
* - drupal.current.xml and semver_test.current.xml: These declare major
* releases supported, 8 and 9.
*/
public function testMajorUpdateAvailable(): void {
foreach (['9.0.0', '8.0.0-9.0.0'] as $release_history) {
foreach ([0, 1] as $minor_version) {
foreach ([0, 1] as $patch_version) {
foreach (['-alpha1', '-beta1', ''] as $extra_version) {
$installed_version = "8.$minor_version.$patch_version$extra_version";
$this->setProjectInstalledVersion($installed_version);
$this->refreshUpdateStatus([$this->updateProject => $release_history]);
$this->standardTests();
$this->drupalGet('admin/reports/updates');
$this->clickLink('Check manually');
$this->checkForMetaRefresh();
$this->assertUpdateTableTextNotContains('Security update required!');
$this->assertUpdateTableElementContains((string) Link::fromTextAndUrl('9.0.0', Url::fromUri("http://example.com/{$this->updateProject}-9-0-0-release"))
->toString());
$this->assertUpdateTableElementContains((string) Link::fromTextAndUrl('Release notes', Url::fromUri("http://example.com/{$this->updateProject}-9-0-0-release"))
->toString());
$this->assertUpdateTableTextNotContains('Latest version:');
if ($release_history === '9.0.0') {
$this->assertUpdateTableTextNotContains('Up to date');
$this->assertUpdateTableTextContains('Not supported!');
$this->assertVersionUpdateLinks('Recommended version:', '9.0.0');
$this->assertUpdateTableElementContains('error.svg');
}
else {
if ($installed_version === '8.1.1') {
$this->assertUpdateTableTextContains('Up to date');
}
else {
$this->assertUpdateTableTextNotContains('Up to date');
$this->assertVersionUpdateLinks('Recommended version:', '8.1.1');
}
$this->assertUpdateTableTextNotContains('Not supported!');
$this->assertVersionUpdateLinks('Also available:', '9.0.0');
$this->assertUpdateTableElementNotContains('error.svg');
}
}
}
}
}
}
/**
* Tests messages when a project release is unpublished.
*
* This test confirms that revoked messages are displayed regardless of
* whether the installed version is in a supported branch or not. This test
* relies on 2 test XML fixtures that are identical except for the
* 'supported_branches' value:
* - [::$updateProject].8.1.0.xml
* 'supported_branches' is '8.0.,8.1.'.
* - [::$updateProject].8.1.0-unsupported.xml
* 'supported_branches' is '8.1.'.
* They both have an '8.0.2' release that is unpublished and an '8.1.0'
* release that is published and is the expected update.
*/
public function testRevokedRelease(): void {
foreach (['8.1.0', '8.1.0-unsupported'] as $fixture) {
$this->setProjectInstalledVersion('8.0.2');
$this->refreshUpdateStatus([$this->updateProject => $fixture]);
$this->standardTests();
$this->confirmRevokedStatus('8.0.2', '8.1.0', 'Recommended version:');
}
}
/**
* Tests messages when a project release is marked unsupported.
*
* This test confirms unsupported messages are displayed regardless of whether
* the installed version is in a supported branch or not. This test relies on
* 2 test XML fixtures that are identical except for the 'supported_branches'
* value:
* - [::$updateProject].8.1.0.xml
* 'supported_branches' is '8.0.,8.1.'.
* - [::$updateProject].8.1.0-supported.xml
* 'supported_branches' is '8.1.,9.0.,10.0.'
* - [::$updateProject].8.1.0-unsupported.xml
* 'supported_branches' is '8.0.'.
* - [::$updateProject].8.1.0-unsupported.xml
* 'supported_branches' is '8.1.'.
* They both have an '8.0.3' release that has the 'Release type' value of
* 'unsupported' and an '8.1.0' release that has the 'Release type' value of
* 'supported' and is the expected update.
*/
public function testUnsupportedRelease(): void {
foreach (['8.1.0', '8.1.0-unsupported'] as $fixture) {
$this->setProjectInstalledVersion('8.0.3');
$this->refreshUpdateStatus([$this->updateProject => $fixture]);
$this->standardTests();
$this->confirmUnsupportedStatus('8.0.3', '8.1.0', 'Recommended version:');
}
// Test when the newest branch is unsupported and no update is available.
foreach (['8.1.0', '8.1.0-beta1'] as $version) {
$this->setProjectInstalledVersion($version);
$this->refreshUpdateStatus([$this->updateProject => '1.1-unsupported']);
$this->standardTests();
$this->confirmUnsupportedStatus($version);
}
// Test when the newest branch is supported.
$this->setProjectInstalledVersion('8.0.3');
$this->refreshUpdateStatus([$this->updateProject => '1.0-supported']);
$this->standardTests();
$this->confirmUnsupportedStatus('8.0.3', '8.1.0', 'Recommended version:');
$this->assertVersionUpdateLinks('Also available', '10.0.0');
$this->assertVersionUpdateLinks('Also available', '9.0.0', 1);
}
}

View File

@@ -0,0 +1,222 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Functional;
/**
* Provides a test and data provider for semver security availability tests.
*/
trait UpdateSemverTestSecurityAvailabilityTrait {
/**
* Tests the Update Manager module when a security update is available.
*
* @param string $site_patch_version
* The patch version to set the site to for testing.
* @param string[] $expected_security_releases
* The security releases, if any, that the status report should recommend.
* @param string $expected_update_message_type
* The type of update message expected.
* @param string $fixture
* The test fixture that contains the test XML.
*
* @dataProvider securityUpdateAvailabilityProvider
*/
public function testSecurityUpdateAvailability($site_patch_version, array $expected_security_releases, $expected_update_message_type, $fixture): void {
$this->setProjectInstalledVersion("8.$site_patch_version");
$this->refreshUpdateStatus([$this->updateProject => $fixture]);
$this->assertSecurityUpdates("{$this->updateProject}-8", $expected_security_releases, $expected_update_message_type, $this->updateTableLocator);
}
/**
* Data provider method for testSecurityUpdateAvailability().
*
* These test cases rely on the following fixtures containing the following
* releases:
* - [::$updateProject].sec.8.0.1_0.2.xml
* - 8.0.2 Security update
* - 8.0.1 Security update, Insecure
* - 8.0.0 Insecure
* - [::$updateProject].sec.8.0.2.xml
* - 8.0.2 Security update
* - 8.0.1 Insecure
* - 8.0.0 Insecure
* - [::$updateProject].sec.8.2.0-rc2.xml
* - 8.2.0-rc2 Security update
* - 8.2.0-rc1 Insecure
* - 8.2.0-beta2 Insecure
* - 8.2.0-beta1 Insecure
* - 8.2.0-alpha2 Insecure
* - 8.2.0-alpha1 Insecure
* - 8.1.2 Security update
* - 8.1.1 Insecure
* - 8.1.0 Insecure
* - 8.0.2 Security update
* - 8.0.1 Insecure
* - 8.0.0 Insecure
* - [::$updateProject].sec.8.1.2.xml
* - 8.1.2 Security update
* - 8.1.1 Insecure
* - 8.1.0 Insecure
* - 8.0.2
* - 8.0.1
* - 8.0.0
* - [::$updateProject].sec.8.1.2_insecure.xml
* - 8.1.2 Security update
* - 8.1.1 Insecure
* - 8.1.0 Insecure
* - 8.0.2 Insecure
* - 8.0.1 Insecure
* - 8.0.0 Insecure
* - [::$updateProject].sec.8.1.2_insecure-unsupported
* This file has the exact releases as
* [::$updateProject].sec.8.1.2_insecure.xml. It has a different value for
* 'supported_branches' that does not contain '8.0.'. It is used to ensure
* that the "Security update required!" is displayed even if the currently
* installed version is in an unsupported branch.
* - [::$updateProject].sec.2.0-rc2-b.xml
* - 8.2.0-rc2
* - 8.2.0-rc1
* - 8.2.0-beta2
* - 8.2.0-beta1
* - 8.2.0-alpha2
* - 8.2.0-alpha1
* - 8.1.2 Security update
* - 8.1.1 Insecure
* - 8.1.0 Insecure
* - 8.0.2 Security update
* - 8.0.1 Insecure
* - 8.0.0 Insecure
*/
public static function securityUpdateAvailabilityProvider() {
$test_cases = [
// Security release available for site minor release 0.
// No releases for next minor.
'0.0, 0.2' => [
'site_patch_version' => '0.0',
'expected_security_releases' => ['0.2'],
'expected_update_message_type' => static::SECURITY_UPDATE_REQUIRED,
'fixture' => 'sec.8.0.2',
],
// Site on latest security release available for site minor release 0.
// Minor release 1 also has a security release, and the current release
// is marked as insecure.
'0.2, 0.2' => [
'site_patch_version' => '0.2',
'expected_security_releases' => ['1.2', '2.0-rc2'],
'expected_update_message_type' => static::UPDATE_AVAILABLE,
'fixture' => 'sec.8.2.0-rc2',
],
// Two security releases available for site minor release 0.
// 0.1 security release marked as insecure.
// No releases for next minor.
'0.0, 0.1, 0.2' => [
'site_patch_version' => '0.0',
'expected_security_releases' => ['0.2'],
'expected_update_message_type' => static::SECURITY_UPDATE_REQUIRED,
'fixture' => 'sec.8.0.1_8.0.2',
],
// Security release available for site minor release 1.
// No releases for next minor.
'1.0, 1.2' => [
'site_patch_version' => '1.0',
'expected_security_releases' => ['1.2'],
'expected_update_message_type' => static::SECURITY_UPDATE_REQUIRED,
'fixture' => 'sec.8.1.2',
],
// Security release available for site minor release 0.
// Security release also available for next minor.
'0.0, 0.2 1.2' => [
'site_patch_version' => '0.0',
'expected_security_releases' => ['0.2', '1.2', '2.0-rc2'],
'expected_update_message_type' => static::SECURITY_UPDATE_REQUIRED,
'fixture' => 'sec.8.2.0-rc2',
],
// No newer security release for site minor 1.
// Previous minor has security release.
'1.2, 0.2 1.2' => [
'site_patch_version' => '1.2',
'expected_security_releases' => [],
'expected_update_message_type' => static::UPDATE_NONE,
'fixture' => 'sec.8.2.0-rc2',
],
// No security release available for site minor release 0.
// Security release available for next minor.
'0.0, 1.2, insecure' => [
'site_patch_version' => '0.0',
'expected_security_releases' => ['1.2'],
'expected_update_message_type' => static::SECURITY_UPDATE_REQUIRED,
'fixture' => 'sec.8.1.2_insecure',
],
// No security release available for site minor release 0.
// Site minor is not a supported branch.
// Security release available for next minor.
'0.0, 1.2, insecure-unsupported' => [
'site_patch_version' => '0.0',
'expected_security_releases' => ['1.2'],
'expected_update_message_type' => static::SECURITY_UPDATE_REQUIRED,
'fixture' => 'sec.8.1.2_insecure-unsupported',
],
// All releases for minor 0 are secure.
// Security release available for next minor.
'0.0, 1.2, secure' => [
'site_patch_version' => '0.0',
'expected_security_releases' => ['1.2'],
'expected_update_message_type' => static::UPDATE_AVAILABLE,
'fixture' => 'sec.8.1.2',
],
'0.2, 1.2, secure' => [
'site_patch_version' => '0.2',
'expected_security_releases' => ['1.2'],
'expected_update_message_type' => static::UPDATE_AVAILABLE,
'fixture' => 'sec.8.1.2',
],
// Site on 2.0-rc2 which is a security release.
'2.0-rc2, 0.2 1.2' => [
'site_patch_version' => '2.0-rc2',
'expected_security_releases' => [],
'expected_update_message_type' => static::UPDATE_NONE,
'fixture' => 'sec.8.2.0-rc2',
],
// Ensure that 8.0.2 security release is not shown because it is earlier
// version than 1.0.
'1.0, 0.2 1.2' => [
'site_patch_version' => '1.0',
'expected_security_releases' => ['1.2', '2.0-rc2'],
'expected_update_message_type' => static::SECURITY_UPDATE_REQUIRED,
'fixture' => 'sec.8.2.0-rc2',
],
];
$pre_releases = [
'2.0-alpha1',
'2.0-alpha2',
'2.0-beta1',
'2.0-beta2',
'2.0-rc1',
'2.0-rc2',
];
foreach ($pre_releases as $pre_release) {
// If the site is on an alpha/beta/RC of an upcoming minor and none of the
// alpha/beta/RC versions are marked insecure, no security update should
// be required.
$test_cases["Pre-release:$pre_release, no security update"] = [
'site_patch_version' => $pre_release,
'expected_security_releases' => [],
'expected_update_message_type' => $pre_release === '2.0-rc2' ? static::UPDATE_NONE : static::UPDATE_AVAILABLE,
'fixture' => 'sec.8.2.0-rc2-b',
];
// If the site is on an alpha/beta/RC of an upcoming minor and there is
// an RC version with a security update, it should be recommended.
$test_cases["Pre-release:$pre_release, security update"] = [
'site_patch_version' => $pre_release,
'expected_security_releases' => $pre_release === '2.0-rc2' ? [] : ['2.0-rc2'],
'expected_update_message_type' => $pre_release === '2.0-rc2' ? static::UPDATE_NONE : static::SECURITY_UPDATE_REQUIRED,
'fixture' => 'sec.8.2.0-rc2',
];
}
return $test_cases;
}
}

View File

@@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Functional;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the update_settings form.
*
* @group update
* @group Form
*/
class UpdateSettingsFormTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['update'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests the update_settings form.
*/
public function testUpdateSettingsForm(): void {
$url = Url::fromRoute('update.settings');
// Users without the appropriate permissions should not be able to access.
$this->drupalGet($url);
$this->assertSession()->pageTextContains('Access denied');
// Users with permission should be able to access the form.
$permissions = ['administer site configuration'];
$account = $this->setUpCurrentUser([
'name' => 'system_admin',
'pass' => 'adminPass',
], $permissions);
$this->drupalLogin($account);
$this->drupalGet($url);
$this->assertSession()->fieldExists('update_notify_emails');
$values_to_enter = [
'http://example.com',
'sofie@example.com',
'http://example.com/also-not-an-email-address',
'dries@example.com',
];
// Fill in `http://example.com` as the email address to notify. We expect
// this to trigger a validation error, because it's not an email address,
// and for the corresponding form item to be highlighted.
$this->assertSession()->fieldExists('update_notify_emails')->setValue($values_to_enter[0]);
$this->submitForm([], 'Save configuration');
$this->assertSession()->statusMessageNotExists(MessengerInterface::TYPE_STATUS);
$this->assertSession()->statusMessageNotExists(MessengerInterface::TYPE_WARNING);
$this->assertSession()->statusMessageContains('"http://example.com" is not a valid email address.', MessengerInterface::TYPE_ERROR);
$this->assertTrue($this->assertSession()->fieldExists('update_notify_emails')->hasClass('error'));
$this->assertSame([], $this->config('update.settings')->get('notification.emails'));
// Next, set an invalid email addresses, but make sure it's second entry.
$this->assertSession()->fieldExists('update_notify_emails')->setValue(implode("\n", array_slice($values_to_enter, 1, 2)));
$this->submitForm([], 'Save configuration');
$this->assertSession()->statusMessageNotExists(MessengerInterface::TYPE_STATUS);
$this->assertSession()->statusMessageNotExists(MessengerInterface::TYPE_WARNING);
$this->assertSession()->statusMessageContains('"http://example.com/also-not-an-email-address" is not a valid email address.', MessengerInterface::TYPE_ERROR);
$this->assertTrue($this->assertSession()->fieldExists('update_notify_emails')->hasClass('error'));
$this->assertSame([], $this->config('update.settings')->get('notification.emails'));
// Next, set multiple invalid email addresses, and assert the same as above
// except the message should be adjusted now.
$this->assertSession()->fieldExists('update_notify_emails')->setValue(implode("\n", $values_to_enter));
$this->submitForm([], 'Save configuration');
$this->assertSession()->statusMessageNotExists(MessengerInterface::TYPE_STATUS);
$this->assertSession()->statusMessageNotExists(MessengerInterface::TYPE_WARNING);
$this->assertSession()->statusMessageContains('http://example.com, http://example.com/also-not-an-email-address are not valid email addresses.', MessengerInterface::TYPE_ERROR);
$this->assertTrue($this->assertSession()->fieldExists('update_notify_emails')->hasClass('error'));
$this->assertSame([], $this->config('update.settings')->get('notification.emails'));
// Now fill in valid email addresses, now the form should be saved
// successfully.
$this->assertSession()->fieldExists('update_notify_emails')->setValue("$values_to_enter[1]\r\n$values_to_enter[3]");
$this->submitForm([], 'Save configuration');
$this->assertSession()->statusMessageContains('The configuration options have been saved.', MessengerInterface::TYPE_STATUS);
$this->assertSession()->statusMessageNotExists(MessengerInterface::TYPE_WARNING);
$this->assertSession()->statusMessageNotExists(MessengerInterface::TYPE_ERROR);
$this->assertFalse($this->assertSession()->fieldExists('update_notify_emails')->hasClass('error'));
$this->assertSame(['sofie@example.com', 'dries@example.com'], $this->config('update.settings')->get('notification.emails'));
}
}

View File

@@ -0,0 +1,304 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Functional;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Defines some shared functions used by all update tests.
*
* The overarching methodology of these tests is we need to compare a given
* state of installed modules and themes (e.g., version, project grouping,
* timestamps, etc) against a current state of what the release history XML
* files we fetch say is available. We have dummy XML files (in the
* core/modules/update/tests directory) that describe various scenarios of
* what's available for different test projects, and we have dummy .info file
* data (specified via hook_system_info_alter() in the update_test helper
* module) describing what's currently installed. Each test case defines a set
* of projects to install, their current state (via the
* 'update_test_system_info' variable) and the desired available update data
* (via the 'update_test_xml_map' variable), and then performs a series of
* assertions that the report matches our expectations given the specific
* initial state and availability scenario.
*/
abstract class UpdateTestBase extends BrowserTestBase {
use UpdateTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['update', 'update_test'];
/**
* Denotes a security update will be required in the test case.
*/
const SECURITY_UPDATE_REQUIRED = 'SECURITY_UPDATE_REQUIRED';
/**
* Denotes an update will be available in the test case.
*/
const UPDATE_AVAILABLE = 'UPDATE_AVAILABLE';
/**
* Denotes no update will be available in the test case.
*/
const UPDATE_NONE = 'UPDATE_NONE';
/**
* The CSS locator for the update table run asserts on.
*
* @var string
*/
protected $updateTableLocator;
/**
* The project that is being tested.
*
* @var string
*/
protected $updateProject;
/**
* Refreshes the update status based on the desired available update scenario.
*
* @param $xml_map
* Array that maps project names to availability scenarios to fetch. The key
* '#all' is used if a project-specific mapping is not defined.
* @param $url
* (optional) A string containing the URL to fetch update data from.
* Defaults to 'update-test'.
*
* @see \Drupal\update_test\Controller\UpdateTestController::updateTest()
*/
protected function refreshUpdateStatus($xml_map, $url = 'update-test') {
// Tell the Update Manager module to fetch from the URL provided by
// update_test module.
$this->config('update.settings')->set('fetch.url', Url::fromUri('base:' . $url, ['absolute' => TRUE])->toString())->save();
// Save the map for UpdateTestController::updateTest() to use.
$this->mockReleaseHistory($xml_map);
// Manually check the update status.
$this->drupalGet('admin/reports/updates');
$this->clickLink('Check manually');
$this->checkForMetaRefresh();
}
/**
* Runs a series of assertions that are applicable to all update statuses.
*/
protected function standardTests() {
$this->assertSession()->responseContains('<h3>Drupal core</h3>');
// Verify that the link to the Drupal project appears.
$this->assertSession()->linkExists('Drupal');
$this->assertSession()->linkByHrefExists('http://example.com/project/drupal');
$this->assertSession()->pageTextNotContains('No available releases found');
$this->assertSession()->pageTextContains('Last checked:');
// No download URLs should be present.
$this->assertSession()->responseNotContains('.tar.gz');
}
/**
* Asserts the expected security updates are displayed correctly on the page.
*
* @param string $project_path_part
* The project path part needed for the release link.
* @param string[] $expected_security_releases
* The security releases, if any, that the status report should recommend.
* @param string $expected_update_message_type
* The type of update message expected.
* @param string $update_element_css_locator
* The CSS locator for the page element that contains the security updates.
*/
protected function assertSecurityUpdates($project_path_part, array $expected_security_releases, $expected_update_message_type, $update_element_css_locator) {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->standardTests();
$assert_session->elementTextNotContains('css', $update_element_css_locator, 'Not supported');
$all_security_release_urls = array_map(function ($link) {
return $link->getAttribute('href');
}, $page->findAll('css', "$update_element_css_locator .version-security a[href$='-release']"));
if ($expected_security_releases) {
$expected_release_urls = [];
if ($expected_update_message_type === static::SECURITY_UPDATE_REQUIRED) {
$assert_session->elementTextNotContains('css', $update_element_css_locator, 'Update available');
$assert_session->elementTextContains('css', $update_element_css_locator, 'Security update required!');
// Verify that the error icon is found.
$assert_session->responseContains('error.svg');
}
else {
$assert_session->elementTextContains('css', $update_element_css_locator, 'Update available');
$assert_session->elementTextNotContains('css', $update_element_css_locator, 'Security update required!');
}
$assert_session->elementTextNotContains('css', $update_element_css_locator, 'Up to date');
foreach ($expected_security_releases as $expected_security_release) {
$expected_url_version = str_replace('.', '-', $expected_security_release);
$release_url = "http://example.com/$project_path_part-$expected_url_version-release";
$assert_session->responseNotContains("http://example.com/$project_path_part-$expected_url_version.tar.gz");
$expected_release_urls[] = $release_url;
// Ensure the expected links are security links.
$this->assertContains($release_url, $all_security_release_urls, "Release $release_url is a security release link.");
$assert_session->linkByHrefExists($release_url);
}
// Ensure no other links are shown as security releases.
$this->assertEquals([], array_diff($all_security_release_urls, $expected_release_urls));
}
else {
// Ensure there were no security links.
$this->assertEquals([], $all_security_release_urls);
$assert_session->pageTextNotContains('Security update required!');
if ($expected_update_message_type === static::UPDATE_AVAILABLE) {
$assert_session->elementTextContains('css', $update_element_css_locator, 'Update available');
$assert_session->elementTextNotContains('css', $update_element_css_locator, 'Up to date');
}
elseif ($expected_update_message_type === static::UPDATE_NONE) {
$assert_session->elementTextNotContains('css', $update_element_css_locator, 'Update available');
$assert_session->elementTextContains('css', $update_element_css_locator, 'Up to date');
}
else {
$this->fail('Unexpected value for $expected_update_message_type: ' . $expected_update_message_type);
}
}
}
/**
* Asserts that an update version has the correct links.
*
* @param string $label
* The label for the update.
* @param string $version
* The project version.
* @param int $index
* (optional) The index of the link.
*/
protected function assertVersionUpdateLinks($label, $version, int $index = 0) {
$update_element = $this->findUpdateElementByLabel($label, $index);
// In the release notes URL the periods are replaced with dashes.
$url_version = str_replace('.', '-', $version);
$this->assertEquals($update_element->findLink($version)->getAttribute('href'), "http://example.com/{$this->updateProject}-$url_version-release");
$this->assertStringNotContainsString("http://example.com/{$this->updateProject}-$version.tar.gz", $update_element->getOuterHtml());
$this->assertEquals($update_element->findLink('Release notes')->getAttribute('href'), "http://example.com/{$this->updateProject}-$url_version-release");
}
/**
* Confirms messages are correct when a release has been unpublished/revoked.
*
* @param string $revoked_version
* The revoked version that is currently installed.
* @param string $newer_version
* The expected newer version to recommend.
* @param string $new_version_label
* The expected label for the newer version (for example 'Recommended
* version:' or 'Also available:').
*/
protected function confirmRevokedStatus($revoked_version, $newer_version, $new_version_label) {
$this->drupalGet('admin/reports/updates');
$this->clickLink('Check manually');
$this->checkForMetaRefresh();
$this->assertUpdateTableTextContains('Revoked!');
$this->assertUpdateTableTextContains($revoked_version);
$this->assertUpdateTableElementContains('error.svg');
$this->assertUpdateTableTextContains('Release revoked: Your currently installed release has been revoked, and is no longer available for download. Uninstalling everything included in this release or upgrading is strongly recommended!');
$this->assertVersionUpdateLinks($new_version_label, $newer_version);
}
/**
* Confirms messages are correct when a release has been marked unsupported.
*
* @param string $unsupported_version
* The unsupported version that is currently installed.
* @param string|null $newer_version
* (optional) The expected newer version to recommend.
* @param string|null $new_version_label
* (optional) The expected label for the newer version. For example
* 'Recommended version:' or 'Also available:'.
*/
protected function confirmUnsupportedStatus(string $unsupported_version, ?string $newer_version = NULL, ?string $new_version_label = NULL) {
$this->drupalGet('admin/reports/updates');
$this->clickLink('Check manually');
$this->checkForMetaRefresh();
$this->assertUpdateTableTextContains('Not supported!');
$this->assertUpdateTableTextContains($unsupported_version);
$this->assertUpdateTableElementContains('error.svg');
if ($newer_version === NULL) {
$this->assertUpdateTableTextContains('Release not supported: Your currently installed release is now unsupported, is no longer available for download and no update is available. Uninstalling everything included in this release is strongly recommended!');
$this->assertUpdateTableTextNotContains('Recommended version');
}
else {
$this->assertNotEmpty($newer_version);
$this->assertUpdateTableTextContains('Release not supported: Your currently installed release is now unsupported, and is no longer available for download. Uninstalling everything included in this release or upgrading is strongly recommended!');
$this->assertVersionUpdateLinks($new_version_label, $newer_version);
}
}
/**
* Asserts that the update table text contains the specified text.
*
* @param string $text
* The expected text.
*
* @see \Behat\Mink\WebAssert::elementTextContains()
*/
protected function assertUpdateTableTextContains($text) {
$this->assertSession()
->elementTextContains('css', $this->updateTableLocator, $text);
}
/**
* Asserts that the update table text does not contain the specified text.
*
* @param string $text
* The expected text.
*/
protected function assertUpdateTableTextNotContains($text) {
$this->assertSession()->elementTextNotContains('css', $this->updateTableLocator, $text);
}
/**
* Asserts that the update table element HTML contains the specified text.
*
* @param string $text
* The expected text.
*
* @see \Behat\Mink\WebAssert::elementContains()
*/
protected function assertUpdateTableElementContains($text) {
$this->assertSession()
->elementContains('css', $this->updateTableLocator, $text);
}
/**
* Asserts that the update table element HTML contains the specified text.
*
* @param string $text
* The expected text.
*
* @see \Behat\Mink\WebAssert::elementNotContains()
*/
protected function assertUpdateTableElementNotContains($text) {
$this->assertSession()
->elementNotContains('css', $this->updateTableLocator, $text);
}
/**
* Finds an update page element by label.
*
* @param string $label
* The label for the update, for example "Recommended version:" or
* "Latest version:".
* @param int $index
* (optional) The index of the element.
*
* @return \Behat\Mink\Element\NodeElement
* The update element.
*/
protected function findUpdateElementByLabel($label, int $index = 0) {
$update_elements = $this->getSession()->getPage()
->findAll('css', $this->updateTableLocator . " .project-update__version:contains(\"$label\")");
$this->assertGreaterThanOrEqual($index, count($update_elements));
return $update_elements[$index];
}
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Functional;
/**
* Provides a trait to set system info and XML mappings.
*
* @see update_test_system_info_alter
* @see \Drupal\update_test\Controller\UpdateTestController::updateTest
* @see \Drupal\Core\Extension\ExtensionList::doList()
* @see \Drupal\Core\Extension\InfoParserInterface
* @see update_test_system_info_alter
*/
trait UpdateTestTrait {
/**
* Sets information about installed extensions.
*
* @param string[][] $installed_extensions
* An array containing mocked installed extensions info. Keys are
* extension names, values are arrays containing key-value pairs that would
* be present in extensions' *.info.yml files.
* For a list of accepted keys, see InfoParserInterface. Key-value pairs not
* present here will be inherited from $default_info.
* For example:
*
* @code
* 'drupal' => [
* 'project' => 'drupal',
* 'version' => '8.0.0',
* 'hidden' => FALSE,
* ]
* @endcode
*
* @throws \Exception
*/
protected function mockInstalledExtensionsInfo(array $installed_extensions): void {
if (in_array('#all', array_keys($installed_extensions), TRUE)) {
throw new \Exception("#all (default value) shouldn't be set here instead use ::mockDefaultExtensionsInfo().");
}
$system_info = $this->config('update_test.settings')->get('system_info');
$system_info = $installed_extensions + $system_info;
$this->config('update_test.settings')->set('system_info', $system_info)->save();
}
/**
* Sets default information about installed extensions.
*
* @param string[] $default_info
* The *.info.yml key-value pairs to be mocked across all
* extensions. Hence, these can be seen as default/fallback values.
*/
protected function mockDefaultExtensionsInfo(array $default_info): void {
$system_info = $this->config('update_test.settings')->get('system_info');
$system_info = ['#all' => $default_info] + $system_info;
$this->config('update_test.settings')->set('system_info', $system_info)->save();
}
/**
* Sets available release history.
*
* @param string[] $release_history
* The release history XML files to use for particular extension(s). The
* keys are the extension names (use 'drupal' for Drupal core itself), and
* the values are the suffix of the release history XML file to use. For
* example, @code 'drupal' => 'sec.8.0.2' @endcode will map to a file called
* drupal.sec.8.0.2.xml. Look at
* core/modules/update/tests/fixtures/release-history for more release
* history XML examples.
*/
protected function mockReleaseHistory(array $release_history): void {
$this->config('update_test.settings')->set('xml_map', $release_history)->save();
}
}

View File

@@ -0,0 +1,211 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Functional;
use Drupal\Core\Extension\InfoParserDynamic;
use Drupal\Core\Updater\Updater;
use Drupal\Core\Url;
use Drupal\Tests\TestFileCreationTrait;
/**
* Tests the Update Manager module's upload and extraction functionality.
*
* @group update
*/
class UpdateUploadTest extends UpdateUploaderTestBase {
use TestFileCreationTrait {
getTestFiles as drupalGetTestFiles;
}
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['file'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$admin_user = $this->drupalCreateUser([
'administer modules',
'administer software updates',
'administer site configuration',
]);
$this->drupalLogin($admin_user);
}
/**
* Tests upload, extraction, and update of a module.
*/
public function testUploadModule(): void {
// Ensure that the update information is correct before testing.
update_get_available(TRUE);
// Images are not valid archives, so get one and try to install it. We
// need an extra variable to store the result of drupalGetTestFiles()
// since reset() takes an argument by reference and passing in a constant
// emits a notice in strict mode.
$imageTestFiles = $this->drupalGetTestFiles('image');
$invalidArchiveFile = reset($imageTestFiles);
$edit = [
'files[project_upload]' => $invalidArchiveFile->uri,
];
// This also checks that the correct archive extensions are allowed.
$this->drupalGet('admin/modules/install');
$this->submitForm($edit, 'Continue');
$extensions = \Drupal::service('plugin.manager.archiver')->getExtensions();
$this->assertSession()->pageTextContains("Only files with the following extensions are allowed: $extensions.");
$this->assertSession()->addressEquals('admin/modules/install');
// Check to ensure an existing module can't be reinstalled. Also checks that
// the archive was extracted since we can't know if the module is already
// installed until after extraction.
$validArchiveFile = __DIR__ . '/../../aaa_update_test.tar.gz';
$edit = [
'files[project_upload]' => $validArchiveFile,
];
$this->drupalGet('admin/modules/install');
$this->submitForm($edit, 'Continue');
$this->assertSession()->pageTextContains('AAA Update test is already present.');
$this->assertSession()->addressEquals('admin/modules/install');
// Ensure that a new module can be extracted and installed.
$updaters = drupal_get_updaters();
$moduleUpdater = $updaters['module']['class'];
$installedInfoFilePath = $this->container->get('update.root') . '/' . $moduleUpdater::getRootDirectoryRelativePath() . '/update_test_new_module/update_test_new_module.info.yml';
$this->assertFileDoesNotExist($installedInfoFilePath);
$validArchiveFile = __DIR__ . '/../../update_test_new_module/8.x-1.0/update_test_new_module.tar.gz';
$edit = [
'files[project_upload]' => $validArchiveFile,
];
$this->drupalGet('admin/modules/install');
$this->submitForm($edit, 'Continue');
// Check that submitting the form takes the user to authorize.php.
$this->assertSession()->addressEquals('core/authorize.php');
$this->assertSession()->titleEquals('Update manager | Drupal');
// Check for a success message on the page, and check that the installed
// module now exists in the expected place in the filesystem.
$this->assertSession()->pageTextContains("Added / updated update_test_new_module successfully");
$this->assertFileExists($installedInfoFilePath);
// Ensure the links are relative to the site root and not
// core/authorize.php.
$this->assertSession()->linkExists('Add another module');
$this->assertSession()->linkByHrefExists(Url::fromRoute('update.module_install')->toString());
$this->assertSession()->linkExists('Install newly added modules');
$this->assertSession()->linkByHrefExists(Url::fromRoute('system.modules_list')->toString());
$this->assertSession()->linkExists('Administration pages');
$this->assertSession()->linkByHrefExists(Url::fromRoute('system.admin')->toString());
// Ensure we can reach the "Add another module" link.
$this->clickLink('Add another module');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->addressEquals('admin/modules/install');
// Check that the module has the correct version before trying to update
// it. Since the module is installed in sites/simpletest, which only the
// child site has access to, standard module API functions won't find it
// when called here. To get the version, the info file must be parsed
// directly instead.
$info_parser = new InfoParserDynamic(DRUPAL_ROOT);
$info = $info_parser->parse($installedInfoFilePath);
$this->assertEquals('8.x-1.0', $info['version']);
// Install the module.
$this->drupalGet('admin/modules');
$this->submitForm(['modules[update_test_new_module][enable]' => TRUE], 'Install');
// Define the update XML such that the new module downloaded above needs an
// update from 8.x-1.0 to 8.x-1.1.
$this->mockInstalledExtensionsInfo([
'update_test_new_module' => [
'project' => 'update_test_new_module',
],
]);
$xml_mapping = [
'update_test_new_module' => '1_1',
];
$this->refreshUpdateStatus($xml_mapping);
// Run the updates for the new module.
$this->drupalGet('admin/reports/updates/update');
$this->submitForm(['projects[update_test_new_module]' => TRUE], 'Download these updates');
$this->submitForm(['maintenance_mode' => FALSE], 'Continue');
$this->assertSession()->pageTextContains('Update was completed successfully.');
$this->assertSession()->pageTextContains("Added / updated update_test_new_module successfully");
// Parse the info file again to check that the module has been updated to
// 8.x-1.1.
$info = $info_parser->parse($installedInfoFilePath);
$this->assertEquals('8.x-1.1', $info['version']);
}
/**
* Ensures that archiver extensions are properly merged in the UI.
*/
public function testFileNameExtensionMerging(): void {
$this->drupalGet('admin/modules/install');
// Make sure the bogus extension supported by update_test.module is there.
$this->assertSession()->responseMatches('/file extensions are supported:.*update-test-extension/');
// Make sure it didn't clobber the first option from core.
$this->assertSession()->responseMatches('/file extensions are supported:.*tar/');
}
/**
* Checks the messages on update manager pages when missing a security update.
*/
public function testUpdateManagerCoreSecurityUpdateMessages(): void {
$this->mockDefaultExtensionsInfo(['version' => '8.0.0']);
$this->mockReleaseHistory(['drupal' => '0.2-sec']);
$this->config('update.settings')
->set('fetch.url', Url::fromRoute('update_test.update_test')->setAbsolute()->toString())
->save();
// Initialize the update status.
$this->drupalGet('admin/reports/updates');
// Now, make sure none of the Update manager pages have duplicate messages
// about core missing a security update.
$this->drupalGet('admin/modules/install');
$this->assertSession()->pageTextNotContains('There is a security update available for your version of Drupal.');
$this->drupalGet('admin/modules/update');
$this->assertSession()->pageTextNotContains('There is a security update available for your version of Drupal.');
$this->drupalGet('admin/appearance/install');
$this->assertSession()->pageTextNotContains('There is a security update available for your version of Drupal.');
$this->drupalGet('admin/appearance/update');
$this->assertSession()->pageTextNotContains('There is a security update available for your version of Drupal.');
$this->drupalGet('admin/reports/updates/install');
$this->assertSession()->pageTextNotContains('There is a security update available for your version of Drupal.');
$this->drupalGet('admin/reports/updates/update');
$this->assertSession()->pageTextNotContains('There is a security update available for your version of Drupal.');
$this->drupalGet('admin/update/ready');
$this->assertSession()->pageTextNotContains('There is a security update available for your version of Drupal.');
}
/**
* Tests only an *.info.yml file are detected without supporting files.
*/
public function testUpdateDirectory(): void {
$type = Updater::getUpdaterFromDirectory($this->root . '/core/modules/update/tests/modules/aaa_update_test');
$this->assertEquals('Drupal\\Core\\Updater\\Module', $type, 'Detected a Module');
$type = Updater::getUpdaterFromDirectory($this->root . '/core/modules/update/tests/themes/update_test_basetheme');
$this->assertEquals('Drupal\\Core\\Updater\\Theme', $type, 'Detected a Theme.');
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Functional;
use Drupal\Core\DrupalKernel;
/**
* Base test class for tests that test project upload functionality.
*/
abstract class UpdateUploaderTestBase extends UpdateTestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Change the root path which Update Manager uses to install and update
// projects to be inside the testing site directory. See
// \Drupal\update\UpdateRootFactory::get() for equivalent changes to the
// test child site.
$request = \Drupal::request();
$update_root = $this->container->get('update.root') . '/' . DrupalKernel::findSitePath($request);
$this->container->get('update.root')->set($update_root);
// Create the directories within the root path within which the Update
// Manager will install projects.
foreach (drupal_get_updaters() as $updater_info) {
$updater = $updater_info['class'];
$install_directory = $update_root . '/' . $updater::getRootDirectoryRelativePath();
if (!is_dir($install_directory)) {
mkdir($install_directory);
}
}
}
}

View File

@@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\update\UpdateFetcherInterface;
use Drupal\update\UpdateManagerInterface;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\Utils;
/**
* Tests the project data when the installed version is a dev version.
*
* @group update
*/
class DevReleaseTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system', 'update', 'update_test'];
/**
* The http client.
*/
protected Client $client;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// The Update module's default configuration must be installed for our
// fake release metadata to be fetched.
$this->installConfig('update');
$this->installConfig('update_test');
$this->setCoreVersion('8.1.0-dev');
$this->setReleaseMetadata(__DIR__ . '/../../fixtures/release-history/drupal.sec.8.1.0-dev.xml');
}
/**
* Sets the current (running) version of core, as known to the Update module.
*
* @param string $version
* The current version of core.
*/
protected function setCoreVersion(string $version): void {
$this->config('update_test.settings')
->set('system_info.#all.version', $version)
->save();
}
/**
* Sets the release metadata file to use when fetching available updates.
*
* @param string $file
* The path of the XML metadata file to use.
*/
protected function setReleaseMetadata(string $file): void {
$metadata = Utils::tryFopen($file, 'r');
$response = new Response(200, [], Utils::streamFor($metadata));
$handler = new MockHandler([$response]);
$this->client = new Client([
'handler' => HandlerStack::create($handler),
]);
$this->container->set('http_client', $this->client);
}
/**
* Tests security updates when the installed version is a dev version.
*
* The xml fixture used here has two security releases 8.1.2 and 8.1.1.
*
* Here the timestamp for the installed dev version is set to 1280424641.
* 8.1.2 will be shown as security update as the date of this security release
* i.e. 1280424741 is greater than the timestamp of the installed version +
* 100 seconds. 8.1.1 will not be shown as security update because it's date
* i.e. 1280424740 is less than timestamp of the installed version + 100
* seconds.
*/
public function testSecurityUpdates(): void {
$system_info = [
'#all' => [
'version' => '8.1.0-dev',
'datestamp' => '1280424641',
],
];
$project_data = $this->getProjectData($system_info);
$this->assertCount(1, $project_data['drupal']['security updates']);
$this->assertSame('8.1.2', $project_data['drupal']['security updates'][0]['version']);
$this->assertSame(UpdateManagerInterface::NOT_CURRENT, $project_data['drupal']['status']);
}
/**
* Tests security updates are empty with a dev version and an empty timestamp.
*
* Here the timestamp for the installed dev version is set to 0(empty
* timestamp) and according to the current logic for dev installed version,
* no updates will be shown as security update.
*/
public function testSecurityUpdateEmptyProjectTimestamp(): void {
$system_info = [
'#all' => [
'version' => '8.1.0-dev',
'datestamp' => '0',
],
];
$project_data = $this->getProjectData($system_info);
$this->assertArrayNotHasKey('security updates', $project_data['drupal']);
$this->assertSame(UpdateFetcherInterface::NOT_CHECKED, $project_data['drupal']['status']);
$this->assertSame('Unknown release date', (string) $project_data['drupal']['reason']);
}
/**
* Gets project data from update_calculate_project_data().
*
* @param array $system_info
* System test information as used by update_test_system_info_alter().
*
* @return array[]
* The project data as returned by update_calculate_project_data().
*
* @see update_test_system_info_alter()
*/
private function getProjectData(array $system_info): array {
$this->config('update_test.settings')
->set('system_info', $system_info)
->save();
update_storage_clear();
$available = update_get_available(TRUE);
return update_calculate_project_data($available);
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Kernel\Migrate\d6;
use Drupal\Tests\SchemaCheckTestTrait;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
/**
* Upgrade variables to update.settings.yml.
*
* @group migrate_drupal_6
*/
class MigrateUpdateConfigsTest extends MigrateDrupal6TestBase {
use SchemaCheckTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['update'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->executeMigration('update_settings');
}
/**
* Tests migration of update variables to update.settings.yml.
*/
public function testUpdateSettings(): void {
$config = $this->config('update.settings');
$this->assertSame(2, $config->get('fetch.max_attempts'));
$this->assertSame('https://updates.drupal.org/release-history', $config->get('fetch.url'));
$this->assertSame('all', $config->get('notification.threshold'));
$this->assertSame([], $config->get('notification.emails'));
$this->assertSame(7, $config->get('check.interval_days'));
$this->assertConfigSchema(\Drupal::service('config.typed'), 'update.settings', $config->get());
}
}

View File

@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\update\UpdateManagerInterface;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\Utils;
/**
* Test the values set in update_calculate_project_data().
*
* @group update
*/
class UpdateCalculateProjectDataTest extends KernelTestBase {
/**
* The Guzzle HTTP client.
*
* @var \GuzzleHttp\Client
*/
protected $client;
/**
* {@inheritdoc}
*/
protected static $modules = ['system', 'update', 'update_test'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// The Update module's default configuration must be installed for our
// fake release metadata to be fetched.
$this->installConfig('update');
$this->installConfig('update_test');
$this->setCoreVersion('8.0.1');
}
/**
* Sets the installed version of core, as known to the Update module.
*
* @param string $version
* The core version.
*
* @see update_test_system_info_alter()
*/
protected function setCoreVersion(string $version): void {
$this->config('update_test.settings')
->set('system_info.#all.version', $version)
->save();
}
/**
* Sets the release metadata file to use when fetching available updates.
*
* @param string $file
* The path of the XML metadata file to use.
*/
protected function setReleaseMetadata(string $file): void {
$metadata = Utils::tryFopen($file, 'r');
$response = new Response(200, [], Utils::streamFor($metadata));
$handler = new MockHandler([$response]);
$this->client = new Client([
'handler' => HandlerStack::create($handler),
]);
$this->container->set('http_client', $this->client);
}
/**
* Data provider for testProjectStatus().
*
* The test cases rely on the following fixtures:
* - drupal.project_status.revoked.0.2.xml: Project_status is 'revoked'.
* - drupal.project_status.insecure.0.2.xml: Project_status is 'insecure'.
* - drupal.project_status.unsupported.0.2.xml: Project_status is
* 'unsupported'.
*
* @return array[]
* Test data.
*/
public static function providerProjectStatus(): array {
return [
'revoked' => [
'fixture' => '/../../fixtures/release-history/drupal.project_status.revoked.0.2.xml',
'status' => UpdateManagerInterface::REVOKED,
'label' => 'Project revoked',
'expected_error_message' => 'This project has been revoked, and is no longer available for download. Uninstalling everything included by this project is strongly recommended!',
],
'insecure' => [
'fixture' => '/../../fixtures/release-history/drupal.project_status.insecure.0.2.xml',
'status' => UpdateManagerInterface::NOT_SECURE,
'label' => 'Project not secure',
'expected_error_message' => 'This project has been labeled insecure by the Drupal security team, and is no longer available for download. Immediately uninstalling everything included by this project is strongly recommended!',
],
'unsupported' => [
'fixture' => '/../../fixtures/release-history/drupal.project_status.unsupported.0.2.xml',
'status' => UpdateManagerInterface::NOT_SUPPORTED,
'label' => 'Project not supported',
'expected_error_message' => 'This project is no longer supported, and is no longer available for download. Uninstalling everything included by this project is strongly recommended!',
],
];
}
/**
* Tests the project_status of the project.
*
* @dataProvider providerProjectStatus
*
* @covers update_calculate_project_update_status
*/
public function testProjectStatus(string $fixture, int $status, string $label, string $expected_error_message): void {
update_storage_clear();
$this->setReleaseMetadata(__DIR__ . $fixture);
$available = update_get_available(TRUE);
$project_data = update_calculate_project_data($available);
$this->assertArrayHasKey('status', $project_data['drupal']);
$this->assertEquals($status, $project_data['drupal']['status']);
$this->assertArrayHasKey('extra', $project_data['drupal']);
$this->assertEquals($label, $project_data['drupal']['extra']['0']['label']);
$this->assertEquals($expected_error_message, $project_data['drupal']['extra']['0']['data']);
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Kernel;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the update_delete_file_if_stale() function.
*
* @group update
*/
class UpdateDeleteFileIfStaleTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'update',
];
/**
* Tests the deletion of stale files.
*/
public function testUpdateDeleteFileIfStale(): void {
$file_system = $this->container->get('file_system');
$file_name = $file_system->saveData($this->randomMachineName(), 'public://');
$this->assertNotNull($file_name);
$file_path = $file_system->realpath($file_name);
// During testing, the file change and the stale checking occurs in the same
// request, so the beginning of request will be before the file changes and
// \Drupal::time()->getRequestTime() - $filectime is negative or zero.
// Set the maximum age to a number even smaller than that.
$this->config('system.file')
->set('temporary_maximum_age', 100000)
->save();
// First test that the file is not stale and thus not deleted.
$deleted = update_delete_file_if_stale($file_path);
$this->assertFalse($deleted);
$this->assertFileExists($file_path);
// Set the maximum age to a number smaller than
// \Drupal::time()->getRequestTime() - $filectime.
$this->config('system.file')
->set('temporary_maximum_age', -100000)
->save();
// Now attempt to delete the file; as it should be considered stale, this
// attempt should succeed.
$deleted = update_delete_file_if_stale($file_path);
$this->assertTrue($deleted);
$this->assertFileDoesNotExist($file_path);
}
}

View File

@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Kernel;
use Drupal\Core\Link;
use Drupal\Core\Url;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
/**
* Tests update report functionality.
*
* @covers template_preprocess_update_report
* @group update
*/
class UpdateReportTest extends KernelTestBase {
use UserCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'update',
];
/**
* @dataProvider providerTemplatePreprocessUpdateReport
*/
public function testTemplatePreprocessUpdateReport($variables): void {
\Drupal::moduleHandler()->loadInclude('update', 'inc', 'update.report');
// The function should run without an exception being thrown when the value
// of $variables['data'] is not set or is not an array.
template_preprocess_update_report($variables);
// Test that the key "no_updates_message" has been set.
$this->assertArrayHasKey('no_updates_message', $variables);
}
/**
* Provides data for testTemplatePreprocessUpdateReport().
*
* @return array
* Array of $variables for template_preprocess_update_report().
*/
public static function providerTemplatePreprocessUpdateReport() {
return [
'$variables with data not set' => [
[],
],
'$variables with data as an integer' => [
['data' => 4],
],
'$variables with data as a string' => [
['data' => 'I am a string'],
],
];
}
/**
* Tests the error message when failing to fetch data without dblog installed.
*
* @see template_preprocess_update_fetch_error_message()
*/
public function testTemplatePreprocessUpdateFetchErrorMessageNoDblog(): void {
$build = [
'#theme' => 'update_fetch_error_message',
];
$this->render($build);
$this->assertRaw('Failed to fetch available update data:<ul><li>See <a href="https://www.drupal.org/node/3170647">PHP OpenSSL requirements</a> in the Drupal.org handbook for possible reasons this could happen and what you can do to resolve them.</li><li>Check your local system logs for additional error messages.</li></ul>');
\Drupal::moduleHandler()->loadInclude('update', 'inc', 'update.report');
$variables = [];
template_preprocess_update_fetch_error_message($variables);
$this->assertArrayHasKey('error_message', $variables);
$this->assertEquals('Failed to fetch available update data:', $variables['error_message']['message']['#markup']);
$this->assertArrayHasKey('documentation_link', $variables['error_message']['items']['#items']);
$this->assertArrayHasKey('logs', $variables['error_message']['items']['#items']);
$this->assertArrayNotHasKey('dblog', $variables['error_message']['items']['#items']);
}
/**
* Tests the error message when failing to fetch data with dblog installed.
*
* @see template_preprocess_update_fetch_error_message()
*/
public function testTemplatePreprocessUpdateFetchErrorMessageWithDblog(): void {
\Drupal::moduleHandler()->loadInclude('update', 'inc', 'update.report');
$this->enableModules(['dblog', 'user']);
$this->installEntitySchema('user');
// First, try as a normal user that can't access dblog.
$this->setUpCurrentUser();
$build = [
'#theme' => 'update_fetch_error_message',
];
$this->render($build);
$this->assertRaw('Failed to fetch available update data:<ul><li>See <a href="https://www.drupal.org/node/3170647">PHP OpenSSL requirements</a> in the Drupal.org handbook for possible reasons this could happen and what you can do to resolve them.</li><li>Check your local system logs for additional error messages.</li></ul>');
$variables = [];
template_preprocess_update_fetch_error_message($variables);
$this->assertArrayHasKey('error_message', $variables);
$this->assertEquals('Failed to fetch available update data:', $variables['error_message']['message']['#markup']);
$this->assertArrayHasKey('documentation_link', $variables['error_message']['items']['#items']);
$this->assertArrayHasKey('logs', $variables['error_message']['items']['#items']);
$this->assertArrayNotHasKey('dblog', $variables['error_message']['items']['#items']);
// Now, try as an admin that can access dblog.
$this->setUpCurrentUser([], ['access content', 'access site reports']);
$this->render($build);
$this->assertRaw('Failed to fetch available update data:<ul><li>See <a href="https://www.drupal.org/node/3170647">PHP OpenSSL requirements</a> in the Drupal.org handbook for possible reasons this could happen and what you can do to resolve them.</li><li>Check');
$dblog_url = Url::fromRoute('dblog.overview', [], ['query' => ['type' => ['update']]]);
$this->assertRaw(Link::fromTextAndUrl('your local system logs', $dblog_url)->toString());
$this->assertRaw(' for additional error messages.</li></ul>');
$variables = [];
template_preprocess_update_fetch_error_message($variables);
$this->assertArrayHasKey('error_message', $variables);
$this->assertEquals('Failed to fetch available update data:', $variables['error_message']['message']['#markup']);
$this->assertArrayHasKey('documentation_link', $variables['error_message']['items']['#items']);
$this->assertArrayNotHasKey('logs', $variables['error_message']['items']['#items']);
$this->assertArrayHasKey('dblog', $variables['error_message']['items']['#items']);
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Kernel;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the Update module storage is cleared correctly.
*
* @group update
*/
class UpdateStorageTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'update',
];
/**
* Tests the Update module storage is cleared correctly.
*/
public function testUpdateStorage(): void {
// Setting values in both key stores, then installing the module and
// testing if these key values are cleared.
$keyvalue_update = $this->container->get('keyvalue.expirable')->get('update');
$keyvalue_update->set('key', 'some value');
$keyvalue_update_available_release = $this->container->get('keyvalue.expirable')->get('update_available_release');
$keyvalue_update_available_release->set('key', 'some value');
$this->container->get('module_installer')->install(['help']);
$this->assertNull($keyvalue_update->get('key'));
$this->assertNull($keyvalue_update_available_release->get('key'));
// Setting new values in both key stores, then uninstalling the module and
// testing if these new key values are cleared.
$keyvalue_update->set('another_key', 'some value');
$keyvalue_update_available_release->set('another_key', 'some value');
$this->container->get('module_installer')->uninstall(['help']);
$this->assertNull($keyvalue_update->get('another_key'));
$this->assertNull($keyvalue_update_available_release->get('another_key'));
}
}

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Unit\Menu;
use Drupal\Tests\Core\Menu\LocalTaskIntegrationTestBase;
/**
* Tests existence of update local tasks.
*
* @group update
*/
class UpdateLocalTasksTest extends LocalTaskIntegrationTestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
$this->directoryList = ['update' => 'core/modules/update'];
parent::setUp();
}
/**
* Checks update report tasks.
*
* @dataProvider getUpdateReportRoutes
*/
public function testUpdateReportLocalTasks($route): void {
$this->assertLocalTasks($route, [
0 => ['update.status', 'update.settings', 'update.report_update'],
]);
}
/**
* Provides a list of report routes to test.
*/
public static function getUpdateReportRoutes() {
return [
['update.status'],
['update.settings'],
['update.report_update'],
];
}
/**
* Checks update module tasks.
*
* @dataProvider getUpdateModuleRoutes
*/
public function testUpdateModuleLocalTasks($route): void {
$this->assertLocalTasks($route, [
0 => ['update.module_update'],
]);
}
/**
* Provides a list of module routes to test.
*/
public static function getUpdateModuleRoutes() {
return [
['update.module_update'],
];
}
/**
* Checks update theme tasks.
*
* @dataProvider getUpdateThemeRoutes
*/
public function testUpdateThemeLocalTasks($route): void {
$this->assertLocalTasks($route, [
0 => ['update.theme_update'],
]);
}
/**
* Provides a list of theme routes to test.
*/
public static function getUpdateThemeRoutes() {
return [
['update.theme_update'],
];
}
}

View File

@@ -0,0 +1,206 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Unit;
use Drupal\Tests\UnitTestCase;
use Drupal\update\ProjectCoreCompatibility;
/**
* @coversDefaultClass \Drupal\update\ProjectCoreCompatibility
*
* @group update
*/
class ProjectCoreCompatibilityTest extends UnitTestCase {
/**
* @covers ::setReleaseMessage
* @dataProvider providerSetProjectCoreCompatibilityRanges
*/
public function testSetProjectCoreCompatibilityRanges(array $project_data, $core_data, array $supported_branches, array $core_releases, array $expected_releases, array $expected_security_updates): void {
$project_compatibility = new ProjectCoreCompatibility($core_data, $core_releases, $supported_branches);
$project_compatibility->setStringTranslation($this->getStringTranslationStub());
$project_compatibility->setReleaseMessage($project_data);
$this->assertSame($expected_releases, $project_data['releases']);
$this->assertSame($expected_security_updates, $project_data['security updates']);
}
/**
* Data provider for testSetProjectCoreCompatibilityRanges().
*/
public static function providerSetProjectCoreCompatibilityRanges() {
$test_cases['no 9 releases, no supported branches'] = [
'project_data' => [
'recommended' => '1.0.1',
'latest_version' => '1.2.3',
'also' => [
'1.2.4',
'1.2.5',
'1.2.6',
],
'releases' => [
'1.0.1' => [
'core_compatibility' => '8.x',
],
'1.2.3' => [
'core_compatibility' => '^8.9 || ^9',
],
'1.2.4' => [
'core_compatibility' => '^8.9.2 || ^9',
],
'1.2.6' => [],
],
'security updates' => [
'1.2.5' => [
'core_compatibility' => '8.9.0 || 8.9.2 || ^9.0.1',
],
],
],
'core_data' => [
'existing_version' => '8.8.0',
],
'supported_branches' => [],
'core_releases' => [
'8.8.0-alpha1' => [],
'8.8.0-beta1' => [],
'8.8.0-rc1' => [],
'8.8.0' => [],
'8.8.1' => [],
'8.8.2' => [],
'8.9.0' => [],
'8.9.1' => [],
'8.9.2' => [],
],
];
// Confirm that with no core supported branches the releases are not changed.
$test_cases['no 9 releases, no supported branches'] += [
'expected_releases' => $test_cases['no 9 releases, no supported branches']['project_data']['releases'],
'expected_security_updates' => $test_cases['no 9 releases, no supported branches']['project_data']['security updates'],
];
// Confirm that if core has supported branches the releases will updated
// with 'core_compatible' and 'core_compatibility_message'.
$test_cases['no 9 releases'] = $test_cases['no 9 releases, no supported branches'];
$test_cases['no 9 releases']['supported_branches'] = ['8.8.', '8.9.'];
$test_cases['no 9 releases']['expected_releases'] = [
'1.0.1' => [
'core_compatibility' => '8.x',
'core_compatible' => TRUE,
'core_compatibility_message' => 'Requires Drupal core: 8.8.0 to 8.9.2',
],
'1.2.3' => [
'core_compatibility' => '^8.9 || ^9',
'core_compatible' => FALSE,
'core_compatibility_message' => 'Requires Drupal core: 8.9.0 to 8.9.2',
],
'1.2.4' => [
'core_compatibility' => '^8.9.2 || ^9',
'core_compatible' => FALSE,
'core_compatibility_message' => 'Requires Drupal core: 8.9.2',
],
'1.2.6' => [],
];
$test_cases['no 9 releases']['expected_security_updates'] = [
'1.2.5' => [
'core_compatibility' => '8.9.0 || 8.9.2 || ^9.0.1',
'core_compatible' => FALSE,
'core_compatibility_message' => 'Requires Drupal core: 8.9.0, 8.9.2',
],
];
// Ensure that when only Drupal 9 pre-releases none of the expected ranges
// change.
$test_cases['with 9 pre releases'] = $test_cases['no 9 releases'];
$test_cases['with 9 pre releases']['core_releases'] += [
'9.0.0-alpha1' => [],
'9.0.0-beta1' => [],
'9.0.0-rc1' => [],
];
// Ensure that when the Drupal 9 full releases are added but they are not
// supported none of the expected ranges change.
$test_cases['with 9 full releases, not supported'] = $test_cases['with 9 pre releases'];
$test_cases['with 9 full releases, not supported']['core_releases'] += [
'9.0.0' => [],
'9.0.1' => [],
'9.0.2' => [],
];
// Ensure that when the Drupal 9 full releases are supported the expected
// ranges do change.
$test_cases['with 9 full releases, supported'] = $test_cases['with 9 full releases, not supported'];
$test_cases['with 9 full releases, supported']['supported_branches'][] = '9.0.';
$test_cases['with 9 full releases, supported']['expected_releases'] = [
'1.0.1' => [
'core_compatibility' => '8.x',
'core_compatible' => TRUE,
'core_compatibility_message' => 'Requires Drupal core: 8.8.0 to 8.9.2',
],
'1.2.3' => [
'core_compatibility' => '^8.9 || ^9',
'core_compatible' => FALSE,
'core_compatibility_message' => 'Requires Drupal core: 8.9.0 to 9.0.2',
],
'1.2.4' => [
'core_compatibility' => '^8.9.2 || ^9',
'core_compatible' => FALSE,
'core_compatibility_message' => 'Requires Drupal core: 8.9.2 to 9.0.2',
],
'1.2.6' => [],
];
$test_cases['with 9 full releases, supported']['expected_security_updates'] = [
'1.2.5' => [
'core_compatibility' => '8.9.0 || 8.9.2 || ^9.0.1',
'core_compatible' => FALSE,
'core_compatibility_message' => 'Requires Drupal core: 8.9.0, 8.9.2, 9.0.1 to 9.0.2',
],
];
return $test_cases;
}
/**
* @covers ::isCoreCompatible
* @dataProvider providerIsCoreCompatible
*
* @param string $constraint
* The core_version_constraint to test.
* @param string $installed_core
* The installed version of core to compare against.
* @param bool $expected
* The expected result.
*/
public function testIsCoreCompatible(string $constraint, string $installed_core, bool $expected): void {
$core_data['existing_version'] = $installed_core;
$project_compatibility = new ProjectCoreCompatibility($core_data, [], []);
$reflection = new \ReflectionClass(ProjectCoreCompatibility::class);
$reflection_method = $reflection->getMethod('isCoreCompatible');
$result = $reflection_method->invokeArgs($project_compatibility, [$constraint]);
$this->assertSame($expected, $result);
}
/**
* Data provider for testIsCoreCompatible().
*/
public static function providerIsCoreCompatible(): array {
$test_cases['compatible exact'] = [
'10.3.0',
'10.3.0',
TRUE,
];
$test_cases['compatible with OR'] = [
'^9 || ^10',
'10.3.0',
TRUE,
];
$test_cases['incompatible'] = [
'^10',
'11.0.0',
FALSE,
];
$test_cases['broken'] = [
'^^11',
'11.0.0',
FALSE,
];
return $test_cases;
}
}

View File

@@ -0,0 +1,262 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Unit;
use Drupal\Tests\UnitTestCase;
use Drupal\update\ProjectRelease;
/**
* @coversDefaultClass \Drupal\update\ProjectRelease
*
* @group update
*/
class ProjectReleaseTest extends UnitTestCase {
/**
* Tests creating with valid data.
*
* @param mixed[] $data
* The data to test. It will be combined with ::getValidData() results.
* @param mixed[] $expected
* The values expected to be returned from the object methods.
*
* @covers ::createFromArray
* @covers ::isInsecure
* @covers ::isSecurityRelease
* @covers ::isPublished
* @covers ::isUnsupported
* @covers ::isUnsupported
*
* @dataProvider providerCreateFromArray
*/
public function testCreateFromArray(array $data, array $expected = []): void {
$data += $this->getValidData();
$expected += $data;
// If not set provide default values that match ::getValidData().
$expected += [
'is_published' => TRUE,
'is_unsupported' => TRUE,
'is_security_release' => TRUE,
'is_insecure' => TRUE,
];
$release = ProjectRelease::createFromArray($data);
$this->assertInstanceOf(ProjectRelease::class, $release);
$this->assertSame($expected['version'], $release->getVersion());
$this->assertSame($expected['date'], $release->getDate());
$this->assertSame($expected['download_link'], $release->getDownloadUrl());
$this->assertSame($expected['release_link'], $release->getReleaseUrl());
$this->assertSame($expected['core_compatibility_message'], $release->getCoreCompatibilityMessage());
$this->assertSame($expected['core_compatible'], $release->isCoreCompatible());
$this->assertSame($expected['is_published'], $release->isPublished());
$this->assertSame($expected['is_unsupported'], $release->isUnsupported());
$this->assertSame($expected['is_security_release'], $release->isSecurityRelease());
$this->assertSame($expected['is_insecure'], $release->isInsecure());
}
/**
* Data provider for testCreateFromArray().
*
* @return mixed
* Test cases for testCreateFromArray().
*/
public static function providerCreateFromArray(): array {
return [
'default valid' => [
'data' => [],
],
'valid with extra field' => [
'data' => ['extra' => 'This value is ignored and will not trigger a validation error.'],
],
'no release types' => [
'data' => [
'terms' => [
'Release type' => [],
],
],
'expected' => [
'is_unsupported' => FALSE,
'is_security_release' => FALSE,
'is_insecure' => FALSE,
],
],
'unpublished' => [
'data' => [
'status' => 'unpublished',
],
'expected' => [
'is_published' => FALSE,
],
],
'core_compatible false' => [
'data' => [
'core_compatible' => FALSE,
],
],
'core_compatible NULL' => [
'data' => [
'core_compatible' => NULL,
],
],
];
}
/**
* Tests that optional fields can be omitted.
*
* @covers ::createFromArray
*/
public function testOptionalFields(): void {
$data = $this->getValidData();
unset(
$data['core_compatible'],
$data['core_compatibility_message'],
$data['download_link'],
$data['date'],
$data['terms']
);
$release = ProjectRelease::createFromArray($data);
$this->assertNull($release->isCoreCompatible());
$this->assertNull($release->getCoreCompatibilityMessage());
$this->assertNull($release->getDate());
// Confirm that all getters that rely on 'terms' default to FALSE.
$this->assertFalse($release->isSecurityRelease());
$this->assertFalse($release->isInsecure());
$this->assertFalse($release->isUnsupported());
}
/**
* Tests exceptions with missing fields.
*
* @param string $missing_field
* The field to test.
*
* @covers ::createFromArray
*
* @dataProvider providerCreateFromArrayMissingField
*/
public function testCreateFromArrayMissingField(string $missing_field): void {
$data = $this->getValidData();
unset($data[$missing_field]);
$this->expectException(\UnexpectedValueException::class);
$expected_message = 'Malformed release data:.*' . preg_quote("[$missing_field]:", '/');
$expected_message .= '.*This field is missing';
$this->expectExceptionMessageMatches("/$expected_message/s");
ProjectRelease::createFromArray($data);
}
/**
* Data provider for testCreateFromArrayMissingField().
*/
public static function providerCreateFromArrayMissingField(): array {
return [
'status' => ['status'],
'version' => ['version'],
'release_link' => ['release_link'],
];
}
/**
* Tests exceptions for invalid field types.
*
* @param string $invalid_field
* The field to test for an invalid value.
* @param mixed $invalid_value
* The invalid value to use in the field.
* @param string $expected_message
* The expected message for the field.
*
* @covers ::createFromArray
*
* @dataProvider providerCreateFromArrayInvalidField
*/
public function testCreateFromArrayInvalidField(string $invalid_field, $invalid_value, string $expected_message): void {
$data = $this->getValidData();
// Set the field a value that is not valid for any of the fields in the
// feed.
$data[$invalid_field] = $invalid_value;
$this->expectException(\UnexpectedValueException::class);
$expected_exception_message = 'Malformed release data:.*' . preg_quote("[$invalid_field]:", '/');
$expected_exception_message .= ".*$expected_message";
$this->expectExceptionMessageMatches("/$expected_exception_message/s");
ProjectRelease::createFromArray($data);
}
/**
* Data provider for testCreateFromArrayInvalidField().
*/
public static function providerCreateFromArrayInvalidField(): array {
return [
'status other' => [
'invalid_field' => 'status',
'invalid_value' => 'other',
'expected_message' => 'The value you selected is not a valid choice.',
],
'status non-string' => [
'invalid_field' => 'status',
'invalid_value' => new \stdClass(),
'expected_message' => 'The value you selected is not a valid choice.',
],
'terms non-array' => [
'invalid_field' => 'terms',
'invalid_value' => 'Unsupported',
'expected_message' => 'This value should be of type array.',
],
'version blank' => [
'invalid_field' => 'version',
'invalid_value' => '',
'expected_message' => 'This value should not be blank.',
],
'core_compatibility_message blank' => [
'invalid_field' => 'core_compatibility_message',
'invalid_value' => '',
'expected_message' => 'This value should not be blank.',
],
'download_link blank' => [
'invalid_field' => 'download_link',
'invalid_value' => '',
'expected_message' => 'This value should not be blank.',
],
'release_link blank' => [
'invalid_field' => 'release_link',
'invalid_value' => '',
'expected_message' => 'This value should not be blank.',
],
'date non-numeric' => [
'invalid_field' => 'date',
'invalid_value' => '2 weeks ago',
'expected_message' => 'This value should be of type numeric.',
],
'core_compatible string' => [
'invalid_field' => 'core_compatible',
'invalid_value' => 'Nope',
'expected_message' => 'This value should be of type boolean.',
],
];
}
/**
* Gets valid data for a project release.
*
* @return mixed[]
* The data for the project release.
*/
protected function getValidData(): array {
return [
'status' => 'published',
'release_link' => 'https://example.com/release-link',
'version' => '8.0.0',
'download_link' => 'https://example.com/download-link',
'core_compatibility_message' => 'This is compatible',
'date' => 1452229200,
'terms' => [
'Release type' => ['Security update', 'Unsupported', 'Insecure'],
],
'core_compatible' => TRUE,
];
}
}

View File

@@ -0,0 +1,240 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Unit;
use ColinODell\PsrTestLogger\TestLogger;
use Drupal\Core\Site\Settings;
use Drupal\Tests\UnitTestCase;
use Drupal\update\UpdateFetcher;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Psr7\Response;
use Psr\Log\LoggerInterface;
/**
* Tests update functionality unrelated to the database.
*
* @coversDefaultClass \Drupal\update\UpdateFetcher
*
* @group update
*/
class UpdateFetcherTest extends UnitTestCase {
/**
* The update fetcher to use.
*
* @var \Drupal\update\UpdateFetcher
*/
protected $updateFetcher;
/**
* History of requests/responses.
*
* @var array
*/
protected $history = [];
/**
* Mock HTTP client.
*
* @var \GuzzleHttp\ClientInterface
*/
protected $mockHttpClient;
/**
* Mock config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $mockConfigFactory;
/**
* A test project to fetch with.
*
* @var array
*/
protected $testProject;
/**
* The logger.
*
* @var \Psr\Log\LoggerInterface
*/
protected LoggerInterface $logger;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->mockConfigFactory = $this->getConfigFactoryStub(['update.settings' => ['fetch_url' => 'http://www.example.com']]);
$this->mockHttpClient = $this->createMock('\GuzzleHttp\ClientInterface');
$settings = new Settings([]);
$this->logger = new TestLogger();
$this->updateFetcher = new UpdateFetcher($this->mockConfigFactory, $this->mockHttpClient, $settings, $this->logger);
$this->testProject = [
'name' => 'update_test',
'project_type' => '',
'info' => [
'version' => '',
'project status url' => 'https://www.example.com',
],
'includes' => ['module1' => 'Module 1', 'module2' => 'Module 2'],
];
}
/**
* Tests that buildFetchUrl() builds the URL correctly.
*
* @param array $project
* A keyed array of project information matching results from
* \Drupal\update\UpdateManager::getProjects().
* @param string $site_key
* A string to mimic an anonymous site key hash.
* @param string $expected
* The expected URL returned from UpdateFetcher::buildFetchUrl()
*
* @dataProvider providerTestUpdateBuildFetchUrl
*
* @see \Drupal\update\UpdateFetcher::buildFetchUrl()
*/
public function testUpdateBuildFetchUrl(array $project, $site_key, $expected): void {
$url = $this->updateFetcher->buildFetchUrl($project, $site_key);
$this->assertEquals($url, $expected);
$this->assertFalse($this->logger->hasErrorRecords());
}
/**
* Provide test data for self::testUpdateBuildFetchUrl().
*
* @return array
* An array of arrays, each containing:
* - 'project' - An array matching a project's .info file structure.
* - 'site_key' - An arbitrary site key.
* - 'expected' - The expected URL from UpdateFetcher::buildFetchUrl().
*/
public static function providerTestUpdateBuildFetchUrl() {
$data = [];
// First test that we didn't break the trivial case.
$project['name'] = 'update_test';
$project['project_type'] = '';
$project['info']['version'] = '';
$project['info']['project status url'] = 'http://www.example.com';
$project['includes'] = ['module1' => 'Module 1', 'module2' => 'Module 2'];
$site_key = '';
$expected = "http://www.example.com/{$project['name']}/current";
$data[] = [$project, $site_key, $expected];
// For uninstalled projects it shouldn't add the site key either.
$site_key = 'site_key';
$project['project_type'] = 'disabled';
$expected = "http://www.example.com/{$project['name']}/current";
$data[] = [$project, $site_key, $expected];
// For installed projects, test adding the site key.
$project['project_type'] = '';
$expected = "http://www.example.com/{$project['name']}/current";
$expected .= '?site_key=site_key';
$expected .= '&list=' . rawurlencode('module1,module2');
$data[] = [$project, $site_key, $expected];
// Test when the URL contains a question mark.
$project['info']['project status url'] = 'http://www.example.com/?project=';
$expected = "http://www.example.com/?project=/{$project['name']}/current";
$expected .= '&site_key=site_key';
$expected .= '&list=' . rawurlencode('module1,module2');
$data[] = [$project, $site_key, $expected];
return $data;
}
/**
* Mocks the HTTP client.
*
* @param \GuzzleHttp\Psr7\Response ...
* Variable number of Response objects that the mocked client should return.
*/
protected function mockClient(Response ...$responses) {
// Create a mock and queue responses.
$mock_handler = new MockHandler($responses);
$handler_stack = HandlerStack::create($mock_handler);
$history = Middleware::history($this->history);
$handler_stack->push($history);
$this->mockHttpClient = new Client(['handler' => $handler_stack]);
}
/**
* @covers ::doRequest
* @covers ::fetchProjectData
*/
public function testUpdateFetcherNoFallback(): void {
// First, try without the HTTP fallback setting, and HTTPS mocked to fail.
$settings = new Settings([]);
$this->mockClient(
new Response(500, [], 'HTTPS failed'),
);
$update_fetcher = new UpdateFetcher($this->mockConfigFactory, $this->mockHttpClient, $settings, $this->logger);
$data = $update_fetcher->fetchProjectData($this->testProject, '');
// There should only be one request / response pair.
$this->assertCount(1, $this->history);
$request = $this->history[0]['request'];
$this->assertNotEmpty($request);
// It should have only been an HTTPS request.
$this->assertEquals('https', $request->getUri()->getScheme());
// And it should have failed.
$response = $this->history[0]['response'];
$this->assertEquals(500, $response->getStatusCode());
$this->assertEmpty($data);
$this->assertTrue($this->logger->hasErrorThatPasses(function (array $record) {
return $record['context']['@message'] === "Server error: `GET https://www.example.com/update_test/current` resulted in a `500 Internal Server Error` response:\nHTTPS failed\n";
}));
}
/**
* @covers ::doRequest
* @covers ::fetchProjectData
*/
public function testUpdateFetcherHttpFallback(): void {
$settings = new Settings(['update_fetch_with_http_fallback' => TRUE]);
$this->mockClient(
new Response(500, [], 'HTTPS failed'),
new Response(200, [], 'HTTP worked'),
);
$update_fetcher = new UpdateFetcher($this->mockConfigFactory, $this->mockHttpClient, $settings, $this->logger);
$data = $update_fetcher->fetchProjectData($this->testProject, '');
// There should be two request / response pairs.
$this->assertCount(2, $this->history);
// The first should have been HTTPS and should have failed.
$first_try = $this->history[0];
$this->assertNotEmpty($first_try);
$this->assertEquals('https', $first_try['request']->getUri()->getScheme());
$this->assertEquals(500, $first_try['response']->getStatusCode());
// The second should have been the HTTP fallback and should have worked.
$second_try = $this->history[1];
$this->assertNotEmpty($second_try);
$this->assertEquals('http', $second_try['request']->getUri()->getScheme());
$this->assertEquals(200, $second_try['response']->getStatusCode());
// Although this is a bogus mocked response, it's what fetchProjectData()
// should return in this case.
$this->assertEquals('HTTP worked', $data);
$this->assertTrue($this->logger->hasErrorThatPasses(function (array $record) {
return $record['context']['@message'] === "Server error: `GET https://www.example.com/update_test/current` resulted in a `500 Internal Server Error` response:\nHTTPS failed\n";
}));
}
}

View File

@@ -0,0 +1,232 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\update\Unit;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Tests\UnitTestCase;
use Drupal\update\UpdateManagerInterface;
/**
* Tests text of update email.
*
* @covers \update_mail
*
* @group update
*/
class UpdateMailTest extends UnitTestCase {
/**
* The container.
*
* @var \Drupal\Core\DependencyInjection\ContainerBuilder
*/
protected $container;
/**
* The mocked current user service.
*
* @var \Drupal\Core\Session\AccountProxy|\PHPUnit\Framework\MockObject\MockObject
*/
protected $currentUser;
/**
* Mocked language manager.
*
* @var \Drupal\language\ConfigurableLanguageManagerInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $languageManager;
/**
* Mocked config factory.
*
* @var \Drupal\Core\Config\ConfigFactory|\PHPUnit\Framework\MockObject\MockObject
*/
protected $configFactory;
/**
* Mocked URL generator.
*
* @var \Drupal\Core\Render\MetadataBubblingUrlGenerator|\PHPUnit\Framework\MockObject\MockObject
*/
protected $urlGenerator;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
include_once __DIR__ . '/../../../update.module';
// Initialize the container.
$this->container = new ContainerBuilder();
$this->container->set('string_translation', $this->getStringTranslationStub());
\Drupal::setContainer($this->container);
// Get needed mocks.
$this->currentUser = $this->createMock('\Drupal\Core\Session\AccountProxy');
$this->languageManager = $this->createMock('Drupal\language\ConfigurableLanguageManagerInterface');
$this->configFactory = $this->createMock('Drupal\Core\Config\ConfigFactory');
$this->urlGenerator = $this->createMock('\Drupal\Core\Render\MetadataBubblingUrlGenerator');
}
/**
* Test the subject and body of update text.
*
* @dataProvider providerTestUpdateEmail
*/
public function testUpdateEmail($notification_threshold, $params, $authorized, array $expected_body): void {
$langcode = 'en';
$available_updates_url = 'https://example.com/admin/reports/updates';
$update_settings_url = 'https://example.com/admin/reports/updates/settings';
$site_name = 'Test site';
// Initialize update_mail input parameters.
$key = NULL;
$message = [
'langcode' => $langcode,
'subject' => '',
'message' => '',
'body' => [],
];
// Language manager just returns the language.
$this->languageManager
->expects($this->once())
->method('getLanguage')
->willReturn($langcode);
// Create three config entities.
$config_site_name = $this->createMock('Drupal\Core\Config\Config');
$config_site_name
->expects($this->once())
->method('get')
->with('name')
->willReturn($site_name);
$config_notification = $this->createMock('Drupal\Core\Config\Config');
$config_notification
->expects($this->once())
->method('get')
->with('notification.threshold')
->willReturn($notification_threshold);
$this->configFactory
->expects($this->exactly(2))
->method('get')
->willReturnMap([
['system.site', $config_site_name],
['update.settings', $config_notification],
]);
// The calls to generateFromRoute differ if authorized.
$count = 2;
if ($authorized) {
$this->currentUser
->expects($this->once())
->method('hasPermission')
->with('administer software updates')
->willReturn(TRUE);
$count = 3;
}
// When authorized also get the URL for the route 'update.report_update'.
$this->urlGenerator
->expects($this->exactly($count))
->method('generateFromRoute')
->willReturnMap([
['update.status', [], ['absolute' => TRUE, 'language' => $langcode], FALSE, $update_settings_url],
['update.settings', [], ['absolute' => TRUE], FALSE, $available_updates_url],
['update.report_update', [], ['absolute' => TRUE, 'language' => $langcode], FALSE, $available_updates_url],
]);
// Set the container.
$this->container->set('language_manager', $this->languageManager);
$this->container->set('url_generator', $this->urlGenerator);
$this->container->set('config.factory', $this->configFactory);
$this->container->set('current_user', $this->currentUser);
\Drupal::setContainer($this->container);
// Generate the email message.
update_mail($key, $message, $params);
// Confirm the subject.
$this->assertSame("New release(s) available for $site_name", $message['subject']);
// Confirm each part of the body.
if ($authorized) {
$this->assertSame($expected_body[0], $message['body'][0]);
$this->assertSame($expected_body[1], $message['body'][1]);
$this->assertSame($expected_body[2], $message['body'][2]->render());
}
else {
if (empty($params)) {
$this->assertSame($expected_body[0], $message['body'][0]);
$this->assertSame($expected_body[1], $message['body'][1]->render());
}
else {
$this->assertSame($expected_body[0], $message['body'][0]->render());
$this->assertSame($expected_body[1], $message['body'][1]);
$this->assertSame($expected_body[2], $message['body'][2]);
$this->assertSame($expected_body[3], $message['body'][3]->render());
}
}
}
/**
* Provides data for ::testUpdateEmail.
*
* @return array
* - The value of the update setting 'notification.threshold'.
* - An array of parameters for update_mail.
* - TRUE if the user is authorized.
* - An array of message body strings.
*/
public static function providerTestUpdateEmail(): array {
return [
'all' => [
'all',
[],
FALSE,
[
"See the available updates page for more information:\nhttps://example.com/admin/reports/updates/settings",
'Your site is currently configured to send these emails when any updates are available. To get notified only for security updates, https://example.com/admin/reports/updates.',
],
],
'security' => [
'security',
[],
FALSE,
[
"See the available updates page for more information:\nhttps://example.com/admin/reports/updates/settings",
'Your site is currently configured to send these emails only when security updates are available. To get notified for any available updates, https://example.com/admin/reports/updates.',
],
],
// Choose parameters that do not require changes to the mocks.
'not secure' => [
'security',
[
'core' => UpdateManagerInterface::NOT_SECURE,
'contrib' => NULL,
],
FALSE,
[
"There is a security update available for your version of Drupal. To ensure the security of your server, you should update immediately!",
'',
"See the available updates page for more information:\nhttps://example.com/admin/reports/updates/settings",
"Your site is currently configured to send these emails only when security updates are available. To get notified for any available updates, https://example.com/admin/reports/updates.",
],
],
'authorize' => [
'all',
[],
TRUE,
[
"See the available updates page for more information:\nhttps://example.com/admin/reports/updates/settings",
"You can automatically download your missing updates using the Update manager:\nhttps://example.com/admin/reports/updates",
'Your site is currently configured to send these emails when any updates are available. To get notified only for security updates, https://example.com/admin/reports/updates.',
],
],
];
}
}