Initial Drupal 11 with DDEV setup

This commit is contained in:
gluebox
2025-10-08 11:39:17 -04:00
commit 89ef74b305
25344 changed files with 2599172 additions and 0 deletions

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Config\InstallStorage;
use Drupal\Core\Config\StorageInterface;
use Drupal\KernelTests\AssertConfigTrait;
/**
* Provides a class for install profiles to check their installed config.
*/
abstract class ConfigAfterInstallerTestBase extends InstallerTestBase {
use AssertConfigTrait;
/**
* Ensures that all the installed config looks like the exported one.
*
* @param array $skipped_config
* An array of skipped config.
*/
protected function assertInstalledConfig(array $skipped_config) {
$this->addToAssertionCount(1);
/** @var \Drupal\Core\Config\StorageInterface $active_config_storage */
$active_config_storage = $this->container->get('config.storage');
/** @var \Drupal\Core\Config\ConfigManagerInterface $config_manager */
$config_manager = $this->container->get('config.manager');
$default_install_path = 'core/profiles/' . $this->profile . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY;
$profile_config_storage = new FileStorage($default_install_path, StorageInterface::DEFAULT_COLLECTION);
foreach ($profile_config_storage->listAll() as $config_name) {
$result = $config_manager->diff($profile_config_storage, $active_config_storage, $config_name);
$this->assertConfigDiff($result, $config_name, $skipped_config);
}
}
}

View File

@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Database\Database;
use Drupal\Core\DrupalKernel;
use Symfony\Component\HttpFoundation\Request;
/**
* Tests distribution profile support with existing settings.
*
* @group Installer
*/
class DistributionProfileExistingSettingsTest extends InstallerTestBase {
/**
* The distribution profile info.
*
* @var array
*/
protected $info;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function prepareEnvironment(): void {
parent::prepareEnvironment();
$this->info = [
'type' => 'profile',
'core_version_requirement' => '*',
'name' => 'Distribution profile',
'distribution' => [
'name' => 'My Distribution',
'install' => [
'theme' => 'olivero',
],
],
];
// File API functions are not available yet.
$path = $this->siteDirectory . '/profiles/my_distribution';
mkdir($path, 0777, TRUE);
file_put_contents("$path/my_distribution.info.yml", Yaml::encode($this->info));
// Pre-configure hash salt.
// Any string is valid, so simply use the class name of this test.
$this->settings['settings']['hash_salt'] = (object) [
'value' => __CLASS__,
'required' => TRUE,
];
// Pre-configure database credentials.
$connection_info = Database::getConnectionInfo();
unset($connection_info['default']['pdo']);
unset($connection_info['default']['init_commands']);
$this->settings['databases']['default'] = (object) [
'value' => $connection_info,
'required' => TRUE,
];
// Use the kernel to find the site path because the site.path service should
// not be available at this point in the install process.
$site_path = DrupalKernel::findSitePath(Request::createFromGlobals());
// Pre-configure config directories.
$this->settings['settings']['config_sync_directory'] = (object) [
'value' => $site_path . '/files/config_staging',
'required' => TRUE,
];
mkdir($this->settings['settings']['config_sync_directory']->value, 0777, TRUE);
}
/**
* {@inheritdoc}
*/
protected function setUpLanguage(): void {
// Make settings file not writable.
$filename = $this->siteDirectory . '/settings.php';
// Make the settings file read-only.
// Not using File API; a potential error must trigger a PHP warning.
chmod($filename, 0444);
// Verify that the distribution name appears.
$this->assertSession()->pageTextContains($this->info['distribution']['name']);
// Verify that the requested theme is used.
$this->assertSession()->responseContains($this->info['distribution']['install']['theme']);
// Verify that the "Choose profile" step does not appear.
$this->assertSession()->pageTextNotContains('profile');
parent::setUpLanguage();
}
/**
* {@inheritdoc}
*/
protected function setUpProfile(): void {
// This step is skipped, because there is a distribution profile.
}
/**
* {@inheritdoc}
*/
protected function setUpSettings(): void {
// This step should not appear, since settings.php is fully configured
// already.
}
/**
* Confirms that the installation succeeded.
*/
public function testInstalled(): void {
$this->assertSession()->addressEquals('user/1');
$this->assertSession()->statusCodeEquals(200);
// Confirm that we are logged-in after installation.
$this->assertSession()->pageTextContains($this->rootUser->getAccountName());
// Confirm that Drupal recognizes this distribution as the current profile.
$this->assertEquals('my_distribution', \Drupal::installProfile());
$this->assertEquals('my_distribution', $this->config('core.extension')->get('profile'), 'The install profile has been written to core.extension configuration.');
$this->rebuildContainer();
$this->assertEquals('my_distribution', \Drupal::installProfile());
}
}

View File

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Serialization\Yaml;
/**
* Tests distribution profile support.
*
* @group Installer
*/
class DistributionProfileTest extends InstallerTestBase {
/**
* The distribution profile info.
*
* @var array
*/
protected $info;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
protected function prepareEnvironment(): void {
parent::prepareEnvironment();
$this->info = [
'type' => 'profile',
'core_version_requirement' => '*',
'name' => 'Distribution profile',
'distribution' => [
'name' => 'My Distribution',
'install' => [
'theme' => 'claro',
'finish_url' => '/root-user',
],
],
];
// File API functions are not available yet.
$path = $this->siteDirectory . '/profiles/my_distribution';
mkdir($path, 0777, TRUE);
file_put_contents("$path/my_distribution.info.yml", Yaml::encode($this->info));
file_put_contents("$path/my_distribution.install", "<?php function my_distribution_install() {\Drupal::entityTypeManager()->getStorage('path_alias')->create(['path' => '/user/1', 'alias' => '/root-user'])->save();}");
}
/**
* {@inheritdoc}
*/
protected function setUpLanguage(): void {
// Verify that the distribution name appears.
$this->assertSession()->pageTextContains($this->info['distribution']['name']);
// Verify that the distribution name is used in the site title.
$this->assertSession()->titleEquals('Choose language | ' . $this->info['distribution']['name']);
// Verify that the requested theme is used.
$this->assertSession()->responseContains($this->info['distribution']['install']['theme']);
// Verify that the "Choose profile" step does not appear.
$this->assertSession()->pageTextNotContains('profile');
parent::setUpLanguage();
}
/**
* {@inheritdoc}
*/
protected function setUpProfile(): void {
// This step is skipped, because there is a distribution profile.
}
/**
* Confirms that the installation succeeded.
*/
public function testInstalled(): void {
$this->assertSession()->addressEquals('root-user');
$this->assertSession()->statusCodeEquals(200);
// Confirm that we are logged-in after installation.
$this->assertSession()->pageTextContains($this->rootUser->getAccountName());
// Confirm that Drupal recognizes this distribution as the current profile.
$this->assertEquals('my_distribution', \Drupal::installProfile());
$this->assertEquals('my_distribution', $this->config('core.extension')->get('profile'), 'The install profile has been written to core.extension configuration.');
}
}

View File

@ -0,0 +1,147 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Serialization\Yaml;
/**
* Tests distribution profile support with a 'langcode' query string.
*
* @group Installer
*
* @see \Drupal\FunctionalTests\Installer\DistributionProfileTranslationTest
*/
class DistributionProfileTranslationQueryTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected $langcode = 'de';
/**
* The distribution profile info.
*
* @var array
*/
protected $info;
/**
* {@inheritdoc}
*/
protected function prepareEnvironment(): void {
parent::prepareEnvironment();
$this->info = [
'type' => 'profile',
'core_version_requirement' => '*',
'name' => 'Distribution profile',
'distribution' => [
'name' => 'My Distribution',
'langcode' => $this->langcode,
'install' => [
'theme' => 'claro',
],
],
];
// File API functions are not available yet.
$path = $this->root . DIRECTORY_SEPARATOR . $this->siteDirectory . '/profiles/my_distribution';
mkdir($path, 0777, TRUE);
file_put_contents("$path/my_distribution.info.yml", Yaml::encode($this->info));
// Place a custom local translation in the translations directory.
mkdir($this->root . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE);
file_put_contents($this->root . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.de.po', $this->getPo('de'));
file_put_contents($this->root . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.fr.po', $this->getPo('fr'));
}
/**
* {@inheritdoc}
*/
protected function visitInstaller(): void {
// Pass a different language code than the one set in the distribution
// profile. This distribution language should still be used.
// The unrouted URL assembler does not exist at this point, so we build the
// URL ourselves.
$this->drupalGet($GLOBALS['base_url'] . '/core/install.php?langcode=fr');
}
/**
* {@inheritdoc}
*/
protected function setUpLanguage(): void {
// This step is skipped, because the distribution profile uses a fixed
// language.
}
/**
* {@inheritdoc}
*/
protected function setUpProfile(): void {
// This step is skipped, because there is a distribution profile.
}
/**
* {@inheritdoc}
*/
protected function setUpSettings(): void {
// The language should have been automatically detected, all following
// screens should be translated already.
$this->assertSession()->buttonExists('Save and continue de');
$this->translations['Save and continue'] = 'Save and continue de';
// Check the language direction.
$direction = $this->getSession()->getPage()->find('xpath', '/@dir')->getText();
$this->assertEquals('ltr', $direction);
// Verify that the distribution name appears.
$this->assertSession()->pageTextContains($this->info['distribution']['name']);
// Verify that the requested theme is used.
$this->assertSession()->responseContains($this->info['distribution']['install']['theme']);
// Verify that the "Choose profile" step does not appear.
$this->assertSession()->pageTextNotContains('profile');
parent::setUpSettings();
}
/**
* Confirms that the installation succeeded.
*/
public function testInstalled(): void {
$this->assertSession()->addressEquals('user/1');
$this->assertSession()->statusCodeEquals(200);
// Confirm that we are logged-in after installation.
$this->assertSession()->pageTextContains($this->rootUser->getDisplayName());
// Verify German was configured but not English.
$this->drupalGet('admin/config/regional/language');
// cspell:ignore deutsch
$this->assertSession()->pageTextContains('Deutsch');
$this->assertSession()->pageTextNotContains('English');
}
/**
* Returns the string for the test .po file.
*
* @param string $langcode
* The language code.
*
* @return string
* Contents for the test .po file.
*/
protected function getPo($langcode): string {
return <<<PO
msgid ""
msgstr ""
msgid "Save and continue"
msgstr "Save and continue $langcode"
PO;
}
}

View File

@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Serialization\Yaml;
/**
* Tests distribution profile support.
*
* @group Installer
*
* @see \Drupal\FunctionalTests\Installer\DistributionProfileTest
*/
class DistributionProfileTranslationTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected $langcode = 'de';
/**
* The distribution profile info.
*
* @var array
*/
protected $info;
/**
* {@inheritdoc}
*/
protected function prepareEnvironment(): void {
parent::prepareEnvironment();
// We set core_version_requirement to '*' for the test so that it does not
// need to be updated between major versions.
$this->info = [
'type' => 'profile',
'core_version_requirement' => '*',
'name' => 'Distribution profile',
'distribution' => [
'name' => 'My Distribution',
'langcode' => $this->langcode,
'install' => [
'theme' => 'claro',
],
],
];
// File API functions are not available yet.
$path = $this->root . DIRECTORY_SEPARATOR . $this->siteDirectory . '/profiles/my_distribution';
mkdir($path, 0777, TRUE);
file_put_contents("$path/my_distribution.info.yml", Yaml::encode($this->info));
// Place a custom local translation in the translations directory.
mkdir($this->root . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE);
file_put_contents($this->root . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.de.po', $this->getPo('de'));
}
/**
* {@inheritdoc}
*/
protected function setUpLanguage(): void {
// This step is skipped, because the distribution profile uses a fixed
// language.
}
/**
* {@inheritdoc}
*/
protected function setUpProfile(): void {
// This step is skipped, because there is a distribution profile.
}
/**
* {@inheritdoc}
*/
protected function setUpSettings(): void {
// The language should have been automatically detected, all following
// screens should be translated already.
$this->assertSession()->buttonExists('Save and continue de');
$this->translations['Save and continue'] = 'Save and continue de';
// Check the language direction.
$this->assertSession()->elementTextEquals('xpath', '/@dir', 'ltr');
// Verify that the distribution name appears.
$this->assertSession()->pageTextContains($this->info['distribution']['name']);
// Verify that the requested theme is used.
$this->assertSession()->responseContains($this->info['distribution']['install']['theme']);
// Verify that the "Choose profile" step does not appear.
$this->assertSession()->pageTextNotContains('profile');
parent::setUpSettings();
}
/**
* Confirms that the installation succeeded.
*/
public function testInstalled(): void {
$this->assertSession()->addressEquals('user/1');
$this->assertSession()->statusCodeEquals(200);
// Confirm that we are logged-in after installation.
$this->assertSession()->pageTextContains($this->rootUser->getDisplayName());
// Verify German was configured but not English.
$this->drupalGet('admin/config/regional/language');
// cspell:ignore deutsch
$this->assertSession()->pageTextContains('Deutsch');
$this->assertSession()->pageTextNotContains('English');
}
/**
* Returns the string for the test .po file.
*
* @param string $langcode
* The language code.
*
* @return string
* Contents for the test .po file.
*/
protected function getPo($langcode): string {
return <<<PO
msgid ""
msgstr ""
msgid "Save and continue"
msgstr "Save and continue $langcode"
PO;
}
}

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Serialization\Yaml;
use Drupal\Tests\BrowserTestBase;
/**
* Tests drupal_flush_all_caches() during an install.
*
* @group Installer
*/
class DrupalFlushAllCachesInInstallerTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected $profile = 'cache_flush_test';
/**
* {@inheritdoc}
*/
protected function prepareEnvironment(): void {
parent::prepareEnvironment();
$info = [
'type' => 'profile',
'core_version_requirement' => '*',
'name' => 'Cache flush test',
'install' => ['language'],
];
// File API functions are not available yet.
$path = $this->siteDirectory . '/profiles/cache_flush_test';
mkdir($path, 0777, TRUE);
file_put_contents("$path/cache_flush_test.info.yml", Yaml::encode($info));
$php_code = <<<EOF
<?php
function cache_flush_test_install() {
// Note it is bad practice to call this method during hook_install() as it
// results in an additional expensive container rebuild.
drupal_flush_all_caches();
// Ensure services are available after calling drupal_flush_all_caches().
\Drupal::state()->set('cache_flush_test', \Drupal::hasService('language_negotiator'));
}
EOF;
file_put_contents("$path/cache_flush_test.install", $php_code);
}
/**
* Confirms that the installation succeeded.
*/
public function testInstalled(): void {
$this->assertTrue(\Drupal::state()->get('cache_flush_test'));
}
}

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Extension\ModuleUninstallValidatorException;
use Drupal\Tests\BrowserTestBase;
/**
* Tests that an install profile can require modules.
*
* @group Installer
*/
class InstallProfileDependenciesTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected $profile = 'testing_install_profile_dependencies';
/**
* Tests that an install profile can require modules.
*/
public function testUninstallingModules(): void {
$user = $this->drupalCreateUser(['administer modules']);
$this->drupalLogin($user);
$this->drupalGet('admin/modules/uninstall');
$this->assertSession()->fieldDisabled('uninstall[dblog]');
$this->getSession()->getPage()->checkField('uninstall[dependency_foo_test]');
$this->click('#edit-submit');
// Click the confirm button.
$this->click('#edit-submit');
$this->assertSession()->responseContains('The selected modules have been uninstalled.');
// We've uninstalled a module therefore we need to rebuild the container in
// the test runner.
$this->rebuildContainer();
$this->assertFalse($this->container->get('module_handler')->moduleExists('dependency_foo_test'));
try {
$this->container->get('module_installer')->uninstall(['dblog']);
$this->fail('Uninstalled dblog module.');
}
catch (ModuleUninstallValidatorException $e) {
$this->assertStringContainsString("The 'Testing install profile dependencies' install profile requires 'Database Logging'", $e->getMessage());
}
}
}

View File

@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Tests\BrowserTestBase;
/**
* Tests that an install profile can be uninstalled.
*
* @group Extension
*/
class InstallProfileUninstallTest extends BrowserTestBase {
/**
* The profile to install as a basis for testing.
*
* This profile is used because it contains a submodule.
*
* @var string
*/
protected $profile = 'testing_config_import';
/**
* {@inheritdoc}
*/
protected static $modules = ['config'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests a user can uninstall install profiles.
*/
public function testUninstallInstallProfile(): void {
$this->drupalLogin($this->drupalCreateUser(admin: TRUE));
// Ensure that the installation profile is present on the status report.
$this->drupalGet('admin/reports/status');
$this->assertSession()->pageTextContains("Installation profile");
$this->assertSession()->pageTextContains("Testing config import");
// Test uninstalling a module provided by the install profile.
$this->drupalGet('admin/modules/uninstall');
$this->assertSession()->pageTextContains("The install profile 'Testing config import' is providing the following module(s): testing_config_import_module");
$this->assertSession()->fieldDisabled('uninstall[testing_config_import]');
$this->assertSession()->fieldEnabled('uninstall[testing_config_import_module]')->check();
$this->getSession()->getPage()->pressButton('Uninstall');
$this->getSession()->getPage()->pressButton('Uninstall');
$this->assertSession()->pageTextContains('The selected modules have been uninstalled.');
$this->assertSession()->fieldNotExists('uninstall[testing_config_import_module]');
$this->assertSession()->pageTextNotContains("The install profile 'Testing config import' is providing the following module(s): testing_config_import_module");
// Test that we can reinstall the module from the profile.
$this->drupalGet('admin/modules');
$this->assertSession()->pageTextContains('Testing config import module');
$this->assertSession()->fieldEnabled('modules[testing_config_import_module][enable]')->check();
$this->getSession()->getPage()->pressButton('Install');
$this->assertSession()->pageTextContains('Module Testing config import module has been installed.');
// Install a theme provided by the module.
$this->drupalGet('admin/appearance');
$this->clickLink("Install Testing config import theme theme");
$this->assertSession()->pageTextContains("The Testing config import theme theme has been installed.");
// Test that uninstalling the module and then the profile works.
$this->drupalGet('admin/modules/uninstall');
$this->assertSession()->pageTextContains("The install profile 'Testing config import' is providing the following module(s): testing_config_import_module");
$this->assertSession()->pageTextContains("The install profile 'Testing config import' is providing the following theme(s): testing_config_import_theme");
$this->assertSession()->fieldEnabled('uninstall[testing_config_import_module]')->check();
$this->getSession()->getPage()->pressButton('Uninstall');
$this->getSession()->getPage()->pressButton('Uninstall');
$this->assertSession()->pageTextContains('The selected modules have been uninstalled.');
$this->assertSession()->fieldNotExists('uninstall[testing_config_import_module]');
$this->drupalGet('admin/appearance');
$this->clickLink("Uninstall Testing config import theme theme");
$this->assertSession()->pageTextContains("The Testing config import theme theme has been uninstalled.");
$this->drupalGet('admin/modules/uninstall');
$this->assertSession()->pageTextNotContains("The install profile 'Testing config import' is providing the following module(s): testing_config_import_module");
$this->assertSession()->pageTextNotContains("The install profile 'Testing config import' is providing the following theme(s): testing_config_import_theme");
$this->assertSession()->fieldEnabled('uninstall[testing_config_import]')->check();
$this->getSession()->getPage()->pressButton('Uninstall');
$this->assertSession()->pageTextContains('Once uninstalled, the Testing config import profile cannot be reinstalled.');
$this->getSession()->getPage()->pressButton('Uninstall');
$this->assertSession()->pageTextContains('The selected modules have been uninstalled.');
$this->assertSession()->fieldNotExists('uninstall[testing_config_import]');
// Test that the module contained in the profile is no longer available to
// install.
$this->drupalGet('admin/modules');
$this->assertSession()->pageTextNotContains('Testing config import module');
$this->assertSession()->fieldNotExists('modules[testing_config_import_module][enable]');
// Ensure that the installation profile is not present on the status report.
$this->drupalGet('admin/reports/status');
$this->assertSession()->pageTextNotContains("Installation profile");
$this->assertSession()->pageTextNotContains("Testing config import");
}
}

View File

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Database\Database;
/**
* Tests the installer with incorrect connection info in settings.php.
*
* @group Installer
*/
class InstallerBrokenDatabaseCredentialsTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function prepareEnvironment(): void {
parent::prepareEnvironment();
// Pre-configure database credentials in settings.php.
$connection_info = Database::getConnectionInfo();
if ($connection_info['default']['driver'] !== 'mysql') {
$this->markTestSkipped('This test relies on overriding the mysql driver');
}
// Provide incorrect host name and test the new error messages.
$connection_info['default']['host'] = 'localhost';
$this->settings['databases']['default'] = (object) [
'value' => $connection_info,
'required' => TRUE,
];
}
/**
* {@inheritdoc}
*/
protected function setUpSettings(): void {
// This form will never be reached.
}
/**
* {@inheritdoc}
*/
protected function setUpSite(): void {
// This form will never be reached.
}
/**
* Tests the expected requirements problem.
*/
public function testRequirementsProblem(): void {
$this->assertSession()->titleEquals('Requirements problem | Drupal');
$this->assertSession()->pageTextContains('Database settings');
$this->assertSession()->pageTextContains('Resolve all issues below to continue the installation. For help configuring your database server,');
$this->assertSession()->pageTextContains('[Tip: Drupal was attempting to connect to the database server via a socket, but the socket file could not be found. A Unix socket file is used if you do not specify a host name or if you specify the special host name localhost. To connect via TCP/IP use an IP address (127.0.0.1 for IPv4) instead of "localhost". This message normally means that there is no MySQL server running on the system or that you are using an incorrect Unix socket file name when trying to connect to the server.]');
}
}

View File

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Database\Database;
/**
* Tests the installer with incorrect connection info in settings.php.
*
* @group Installer
*/
class InstallerBrokenDatabasePortSettingsTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function prepareEnvironment(): void {
parent::prepareEnvironment();
// Pre-configure database credentials in settings.php.
$connection_info = Database::getConnectionInfo();
if ($connection_info['default']['driver'] !== 'mysql') {
$this->markTestSkipped('This test relies on overriding the mysql driver');
}
// Override the port number to random port.
$connection_info['default']['port'] = 1234;
$this->settings['databases']['default'] = (object) [
'value' => $connection_info,
'required' => TRUE,
];
}
/**
* {@inheritdoc}
*/
protected function setUpSettings(): void {
// This form will never be reached.
}
/**
* {@inheritdoc}
*/
protected function setUpSite(): void {
// This form will never be reached.
}
/**
* Tests the expected requirements problem.
*/
public function testRequirementsProblem(): void {
$this->assertSession()->titleEquals('Requirements problem | Drupal');
$this->assertSession()->pageTextContains('Database settings');
$this->assertSession()->pageTextContains('Resolve all issues below to continue the installation. For help configuring your database server,');
$this->assertSession()->pageTextContains("[Tip: This message normally means that there is no MySQL server running on the system or that you are using an incorrect host name or port number when trying to connect to the server. You should also check that the TCP/IP port you are using has not been blocked by a firewall or port blocking service.]");
}
}

View File

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Component\Utility\Crypt;
/**
* Tests installation when a config_sync_directory is set up but does not exist.
*
* @group Installer
*/
class InstallerConfigDirectorySetNoDirectoryErrorTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The directory where the sync directory should be created during install.
*
* @var string
*/
protected $configDirectory;
/**
* {@inheritdoc}
*/
protected function prepareEnvironment(): void {
parent::prepareEnvironment();
$this->configDirectory = $this->publicFilesDirectory . '/config_' . Crypt::randomBytesBase64();
$this->settings['settings']['config_sync_directory'] = (object) [
'value' => $this->configDirectory . '/sync',
'required' => TRUE,
];
// Create the files directory early so we can test the error case.
mkdir($this->publicFilesDirectory);
// Create a file so the directory can not be created.
file_put_contents($this->configDirectory, 'Test');
}
/**
* Installer step: Configure settings.
*/
protected function setUpSettings(): void {
// This step should not appear as we had a failure prior to the settings
// screen.
}
/**
* {@inheritdoc}
*/
protected function setUpRequirementsProblem(): void {
// The parent method asserts that there are no requirements errors, but
// this test expects a requirements error in the test method below.
// Therefore, we override this method to suppress the parent's assertions.
}
/**
* {@inheritdoc}
*/
protected function setUpSite(): void {
// This step should not appear as we had a failure prior to the settings
// screen.
}
/**
* Verifies that installation failed.
*/
public function testError(): void {
$this->assertSession()->pageTextContains("An automated attempt to create the directory {$this->configDirectory}/sync failed, possibly due to a permissions problem.");
$this->assertDirectoryDoesNotExist($this->configDirectory . '/sync');
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Component\Utility\Crypt;
/**
* Tests the installer when a custom config directory set up but does not exist.
*
* @group Installer
*/
class InstallerConfigDirectorySetNoDirectoryTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The sync directory created during the install.
*
* @var string
*/
protected $syncDirectory;
/**
* {@inheritdoc}
*/
protected function prepareEnvironment(): void {
parent::prepareEnvironment();
$this->syncDirectory = $this->publicFilesDirectory . '/config_' . Crypt::randomBytesBase64() . '/sync';
$this->settings['settings']['config_sync_directory'] = (object) [
'value' => $this->syncDirectory,
'required' => TRUE,
];
}
/**
* Verifies that installation succeeded.
*/
public function testInstaller(): void {
$this->assertSession()->addressEquals('user/1');
$this->assertSession()->statusCodeEquals(200);
$this->assertDirectoryExists($this->syncDirectory);
}
}

View File

@ -0,0 +1,187 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Database\Database;
use Drupal\Core\Installer\Form\SelectProfileForm;
/**
* Provides a base class for testing installing from existing configuration.
*/
abstract class InstallerConfigDirectoryTestBase extends InstallerTestBase {
/**
* This is set by the profile in the core.extension extracted.
*
* If set to FALSE, then the install will proceed without an install profile.
*
* @var string|null|bool
*/
protected $profile = NULL;
/**
* @var bool
*/
protected $existingSyncDirectory = FALSE;
/**
* This copies a source directory to a destination directory recursively.
*
* @param string $source
* Source directory.
* @param string $destination
* Destination directory.
*/
protected function copyDirectory(string $source, string $destination): void {
if (!is_dir($destination)) {
mkdir($destination, 0755, TRUE);
}
$files = scandir($source);
foreach ($files as $file) {
if ($file !== '.' && $file !== '..') {
$sourceFile = $source . '/' . $file;
$destinationFile = $destination . '/' . $file;
if (is_dir($sourceFile)) {
$this->copyDirectory($sourceFile, $destinationFile);
}
else {
copy($sourceFile, $destinationFile);
}
}
}
}
/**
* {@inheritdoc}
*/
protected function prepareEnvironment() {
parent::prepareEnvironment();
if ($this->profile === NULL) {
$core_extension_location = $this->getConfigLocation() . '/core.extension.yml';
$core_extension = Yaml::decode(file_get_contents($core_extension_location));
$this->profile = $core_extension['profile'];
}
if ($this->profile !== FALSE) {
// Create a profile for testing. We set core_version_requirement to '*'
// for the test so that it does not need to be updated between major
// versions.
$info = [
'type' => 'profile',
'core_version_requirement' => '*',
'name' => 'Configuration installation test profile (' . $this->profile . ')',
];
// File API functions are not available yet.
$path = $this->siteDirectory . '/profiles/' . $this->profile;
// Put the sync directory inside the profile.
$config_sync_directory = $path . '/config/sync';
mkdir($path, 0777, TRUE);
file_put_contents("$path/{$this->profile}.info.yml", Yaml::encode($info));
}
else {
// If we have no profile we must use an existing sync directory.
$this->existingSyncDirectory = TRUE;
$config_sync_directory = $this->siteDirectory . '/config/sync';
}
if ($this->existingSyncDirectory) {
$config_sync_directory = $this->siteDirectory . '/config/sync';
$this->settings['settings']['config_sync_directory'] = (object) [
'value' => $config_sync_directory,
'required' => TRUE,
];
}
// Create config/sync directory and extract tarball contents to it.
mkdir($config_sync_directory, 0777, TRUE);
$this->copyDirectory($this->getConfigLocation(), $config_sync_directory);
// Add the module that is providing the database driver to the list of
// modules that can not be uninstalled in the core.extension configuration.
if (file_exists($config_sync_directory . '/core.extension.yml')) {
$core_extension = Yaml::decode(file_get_contents($config_sync_directory . '/core.extension.yml'));
$module = Database::getConnection()->getProvider();
if ($module !== 'core') {
$core_extension['module'][$module] = 0;
$core_extension['module'] = module_config_sort($core_extension['module']);
}
if ($this->profile === FALSE && array_key_exists('profile', $core_extension)) {
// Remove the profile.
unset($core_extension['module'][$core_extension['profile']]);
unset($core_extension['profile']);
// Set a default theme to the first theme that will be installed as this
// can not be retrieved from the profile.
$this->defaultTheme = array_key_first($core_extension['theme']);
}
file_put_contents($config_sync_directory . '/core.extension.yml', Yaml::encode($core_extension));
}
}
/**
* Gets the path to the configuration directory.
*
* The directory will be copied to the install profile's config/sync
* directory for testing.
*
* @return string
* The path to the configuration directory.
*/
abstract protected function getConfigLocation();
/**
* {@inheritdoc}
*/
protected function installParameters() {
$parameters = parent::installParameters();
// The options that change configuration are disabled when installing from
// existing configuration.
unset($parameters['forms']['install_configure_form']['site_name']);
unset($parameters['forms']['install_configure_form']['site_mail']);
unset($parameters['forms']['install_configure_form']['enable_update_status_module']);
unset($parameters['forms']['install_configure_form']['enable_update_status_emails']);
return $parameters;
}
/**
* Confirms that the installation installed the configuration correctly.
*/
public function testConfigSync(): void {
// After installation there is no snapshot and nothing to import.
$change_list = $this->configImporter()->getStorageComparer()->getChangelist();
$expected = [
'create' => [],
// The system.mail is changed configuration because the test system
// changes it to ensure that mails are not sent.
'update' => ['system.mail'],
'delete' => [],
'rename' => [],
];
$this->assertEquals($expected, $change_list);
}
/**
* Installer step: Select installation profile.
*/
protected function setUpProfile() {
if ($this->existingSyncDirectory) {
$edit = [
'profile' => SelectProfileForm::CONFIG_INSTALL_PROFILE_KEY,
];
$this->submitForm($edit, $this->translations['Save and continue']);
}
else {
parent::setUpProfile();
}
}
}

View File

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Database\Database;
/**
* Tests the installer with database errors.
*
* @group Installer
*/
class InstallerDatabaseErrorMessagesTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUpSettings(): void {
// We are creating a table here to force an error in the installer because
// it will try and create the drupal_install_test table as this is part of
// the standard database tests performed by the installer in
// Drupal\Core\Database\Install\Tasks.
$spec = [
'fields' => [
'id' => [
'type' => 'int',
'not null' => TRUE,
],
],
'primary key' => ['id'],
];
Database::getConnection('default')->schema()->createTable('drupal_install_test', $spec);
parent::setUpSettings();
}
/**
* {@inheritdoc}
*/
protected function setUpSite(): void {
// This step should not appear as we had a failure on the settings screen.
}
/**
* Verifies that the error message in the settings step is correct.
*/
public function testSetUpSettingsErrorMessage(): void {
$this->assertSession()->responseContains('<ul><li>Failed to <strong>CREATE</strong> a test table');
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
/**
* Tests the installer with empty settings file.
*
* @group Installer
*/
class InstallerEmptySettingsTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function prepareEnvironment(): void {
parent::prepareEnvironment();
// Create an empty settings.php file.
$path = $this->root . DIRECTORY_SEPARATOR . $this->siteDirectory;
file_put_contents($path . '/settings.php', '');
}
/**
* Verifies that installation succeeded.
*/
public function testInstaller(): void {
$this->assertSession()->addressEquals('user/1');
$this->assertSession()->statusCodeEquals(200);
}
}

View File

@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Database\Database;
/**
* Tests the installer with broken database connection info in settings.php.
*
* @group Installer
*/
class InstallerExistingBrokenDatabaseSettingsTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function prepareEnvironment(): void {
parent::prepareEnvironment();
// Pre-configure database credentials in settings.php.
$connection_info = Database::getConnectionInfo();
if ($connection_info['default']['driver'] !== 'mysql') {
$this->markTestSkipped('This test relies on overriding the mysql driver');
}
// Use a database driver that reports a fake database version that does
// not meet requirements.
unset($connection_info['default']['pdo']);
unset($connection_info['default']['init_commands']);
$connection_info['default']['driver'] = 'DriverTestMysqlDeprecatedVersion';
$namespace = 'Drupal\\driver_test\\Driver\\Database\\DriverTestMysqlDeprecatedVersion';
$connection_info['default']['namespace'] = $namespace;
$connection_info['default']['autoload'] = \Drupal::service('extension.list.database_driver')
->get($namespace)
->getAutoloadInfo()['autoload'];
$this->settings['databases']['default'] = (object) [
'value' => $connection_info,
'required' => TRUE,
];
}
/**
* {@inheritdoc}
*/
protected function setUpSettings(): void {
// This form will never be reached.
}
/**
* {@inheritdoc}
*/
protected function setUpRequirementsProblem(): void {
// The parent method asserts that there are no requirements errors, but
// this test expects a requirements error in the test method below.
// Therefore, we override this method to suppress the parent's assertions.
}
/**
* {@inheritdoc}
*/
protected function setUpSite(): void {
// This form will never be reached.
}
/**
* Tests the expected requirements problem.
*/
public function testRequirementsProblem(): void {
$this->assertSession()->titleEquals('Requirements problem | Drupal');
$this->assertSession()->pageTextContains('Database settings');
$this->assertSession()->pageTextContains('Resolve all issues below to continue the installation. For help configuring your database server,');
$this->assertSession()->pageTextContains('The database server version 10.2.31-MariaDB-1:10.2.31+maria~bionic-log is less than the minimum required version');
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
/**
* Tests installation when a config_sync_directory exists and is set up.
*
* @group Installer
*/
class InstallerExistingConfigDirectoryTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The expected file perms of the folder.
*
* @var int
*/
protected $expectedFilePerms;
/**
* {@inheritdoc}
*/
protected function prepareEnvironment(): void {
parent::prepareEnvironment();
mkdir($this->root . DIRECTORY_SEPARATOR . $this->siteDirectory . '/config_read_only', 0444);
$this->expectedFilePerms = fileperms($this->siteDirectory . '/config_read_only');
$this->settings['settings']['config_sync_directory'] = (object) [
'value' => $this->siteDirectory . '/config_read_only',
'required' => TRUE,
];
}
/**
* Verifies that installation succeeded.
*/
public function testInstaller(): void {
$this->assertSession()->addressEquals('user/1');
$this->assertSession()->statusCodeEquals(200);
$this->assertEquals($this->expectedFilePerms, fileperms($this->siteDirectory . '/config_read_only'));
$this->assertEquals([], glob($this->siteDirectory . '/config_read_only/*'), 'The sync directory is empty after install because it is read-only.');
}
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Database\Database;
/**
* Verifies that installing from existing configuration works.
*
* @group Installer
*/
class InstallerExistingConfigExistingSettingsTest extends InstallerExistingConfigTest {
/**
* {@inheritdoc}
*
* Partially configures a preexisting settings.php file before invoking the
* interactive installer.
*/
protected function prepareEnvironment(): void {
parent::prepareEnvironment();
// Pre-configure hash salt.
// Any string is valid, so simply use the class name of this test.
$this->settings['settings']['hash_salt'] = (object) [
'value' => __CLASS__,
'required' => TRUE,
];
// Pre-configure database credentials.
$connection_info = Database::getConnectionInfo();
unset($connection_info['default']['pdo']);
unset($connection_info['default']['init_commands']);
$this->settings['databases']['default'] = (object) [
'value' => $connection_info,
'required' => TRUE,
];
}
}

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Logger\RfcLogLevel;
/**
* Verifies that installing from existing configuration works.
*
* @group Installer
*/
class InstallerExistingConfigMultilingualTest extends InstallerConfigDirectoryTestBase {
/**
* {@inheritdoc}
*/
protected $profile = 'testing_config_install_multilingual';
/**
* {@inheritdoc}
*/
protected function getConfigLocation(): string {
return __DIR__ . '/../../../fixtures/config_install/multilingual';
}
/**
* {@inheritdoc}
*/
public function testConfigSync(): void {
parent::testConfigSync();
// Ensure no warning, error, critical, alert or emergency messages have been
// logged.
$count = (int) \Drupal::database()->select('watchdog', 'w')->fields('w')->condition('severity', RfcLogLevel::WARNING, '<=')->countQuery()->execute()->fetchField();
$this->assertSame(0, $count);
// Ensure the correct message is logged from locale_config_batch_finished().
$count = (int) \Drupal::database()->select('watchdog', 'w')->fields('w')->condition('message', 'The configuration was successfully updated. %number configuration objects updated.')->countQuery()->execute()->fetchField();
$this->assertSame(1, $count);
}
}

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
/**
* Verifies that profiles invalid config can not be installed.
*
* @group Installer
*/
class InstallerExistingConfigNoConfigTest extends InstallerConfigDirectoryTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected $profile = 'no_config_profile';
/**
* Final installer step: Configure site.
*/
protected function setUpSite(): void {
// There are errors therefore there is nothing to do here.
}
/**
* {@inheritdoc}
*/
protected function getConfigLocation(): string {
return __DIR__ . '/../../../fixtures/config_install/testing_config_install_no_config';
}
/**
* Tests that profiles with an empty config/sync directory do not work.
*/
public function testConfigSync(): void {
$this->assertSession()->titleEquals('Configuration validation | Drupal');
$this->assertSession()->pageTextContains('The configuration synchronization failed validation.');
$this->assertSession()->pageTextContains('This import is empty and if applied would delete all of your configuration, so has been rejected.');
// Ensure there is no continuation button.
$this->assertSession()->pageTextNotContains('Save and continue');
$this->assertSession()->buttonNotExists('edit-submit');
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
/**
* Verifies that installing from existing configuration without a profile works.
*
* @group Installer
*/
class InstallerExistingConfigNoProfileTest extends InstallerExistingConfigTest {
/**
* {@inheritdoc}
*/
protected $profile = FALSE;
}

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
/**
* Testing installing from config without system.site.
*
* @group Installer
*/
class InstallerExistingConfigNoSystemSiteTest extends InstallerConfigDirectoryTestBase {
/**
* {@inheritdoc}
*/
protected function prepareEnvironment(): void {
parent::prepareEnvironment();
// File API functions are not available yet.
unlink($this->siteDirectory . '/profiles/' . $this->profile . '/config/sync/system.site.yml');
}
/**
* {@inheritdoc}
*/
public function setUpSite(): void {
// There are errors. Therefore, there is nothing to do here.
}
/**
* Tests that profiles with no system.site do not work.
*/
public function testConfigSync(): void {
$this->htmlOutput(NULL);
$this->assertSession()->titleEquals('Configuration validation | Drupal');
$this->assertSession()->pageTextContains('The configuration synchronization failed validation.');
$this->assertSession()->pageTextContains('This import does not contain system.site configuration, so has been rejected.');
// Ensure there is no continuation button.
$this->assertSession()->pageTextNotContains('Save and continue');
$this->assertSession()->buttonNotExists('edit-submit');
}
/**
* {@inheritdoc}
*/
protected function getConfigLocation(): string {
return __DIR__ . '/../../../fixtures/config_install/testing_config_install';
}
}

View File

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
/**
* Verifies that profiles with hook_install() can't be installed from config.
*
* @group Installer
*/
class InstallerExistingConfigProfileHookInstallTest extends InstallerConfigDirectoryTestBase {
/**
* {@inheritdoc}
*/
protected $profile = 'config_profile_with_hook_install';
/**
* {@inheritdoc}
*/
protected function visitInstaller(): void {
// Create an .install file with a hook_install() implementation.
$path = $this->siteDirectory . '/profiles/' . $this->profile;
$contents = <<<EOF
<?php
function config_profile_with_hook_install_install() {
}
EOF;
file_put_contents("$path/{$this->profile}.install", $contents);
parent::visitInstaller();
}
/**
* Installer step: Configure settings.
*/
protected function setUpSettings(): void {
// There are errors therefore there is nothing to do here.
}
/**
* {@inheritdoc}
*/
protected function setUpRequirementsProblem(): void {
// The parent method asserts that there are no requirements errors, but
// this test expects a requirements error in the test method below.
// Therefore, we override this method to suppress the parent's assertions.
}
/**
* Final installer step: Configure site.
*/
protected function setUpSite(): void {
// There are errors therefore there is nothing to do here.
}
/**
* {@inheritdoc}
*/
protected function getConfigLocation(): string {
// We're not going to get to the config import stage so this does not
// matter.
return __DIR__ . '/../../../fixtures/config_install/testing_config_install_no_config';
}
/**
* Confirms the installation has failed and the expected error is displayed.
*/
public function testConfigSync(): void {
$this->assertSession()->titleEquals('Requirements problem | Drupal');
$this->assertSession()->pageTextContains($this->profile);
$this->assertSession()->pageTextContains('The selected profile has a hook_install() implementation and therefore can not be installed from configuration.');
}
}

View File

@ -0,0 +1,212 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Component\Serialization\Yaml;
// cSpell:ignore Anónimo Aplicar
/**
* Verifies that installing from existing configuration works.
*
* @group Installer
*/
class InstallerExistingConfigSyncDirectoryMultilingualTest extends InstallerConfigDirectoryTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected $profile = 'testing_config_install_multilingual';
/**
* {@inheritdoc}
*/
protected $existingSyncDirectory = TRUE;
/**
* Installer step: Select installation profile.
*/
protected function setUpProfile() {
// Ensure the site name 'Multilingual' appears as expected in the 'Use
// existing configuration' radio description.
$this->assertSession()->pageTextContains('Install Multilingual using existing configuration.');
return parent::setUpProfile();
}
/**
* {@inheritdoc}
*/
protected function getConfigLocation(): string {
return __DIR__ . '/../../../fixtures/config_install/multilingual';
}
/**
* {@inheritdoc}
*/
protected function prepareEnvironment(): void {
parent::prepareEnvironment();
// Place custom local translations in the translations directory and fix up
// configuration.
mkdir($this->publicFilesDirectory . '/translations', 0777, TRUE);
file_put_contents($this->publicFilesDirectory . '/translations/drupal-8.0.0.es.po', $this->getPo('es'));
$locale_settings = Yaml::decode(file_get_contents($this->siteDirectory . '/config/sync/locale.settings.yml'));
$locale_settings['translation']['use_source'] = 'local';
$locale_settings['translation']['path'] = $this->publicFilesDirectory . '/translations';
file_put_contents($this->siteDirectory . '/config/sync/locale.settings.yml', Yaml::encode($locale_settings));
}
/**
* Confirms that the installation installed the configuration correctly.
*/
public function testConfigSync(): void {
$comparer = $this->configImporter()->getStorageComparer();
$expected_changelist_default_collection = [
'create' => [],
// The system.mail is changed configuration because the test system
// changes it to ensure that mails are not sent.
'update' => ['system.mail'],
'delete' => [],
'rename' => [],
];
$this->assertEquals($expected_changelist_default_collection, $comparer->getChangelist());
$expected_changelist_spanish_collection = [
'create' => [],
// The view was untranslated but the translation exists so the installer
// performs the translation.
'update' => ['views.view.who_s_new'],
'delete' => [],
'rename' => [],
];
$this->assertEquals($expected_changelist_spanish_collection, $comparer->getChangelist(NULL, 'language.es'));
// Ensure that menu blocks have been created correctly.
$this->assertSession()->responseNotContains('This block is broken or missing.');
$this->assertSession()->linkExists('Add content');
// Ensure that the Spanish translation of anonymous is the one from
// configuration and not the PO file.
// cspell:disable-next-line
$this->assertSame('Anónimo', \Drupal::languageManager()->getLanguageConfigOverride('es', 'user.settings')->get('anonymous'));
/** @var \Drupal\locale\StringStorageInterface $locale_storage */
$locale_storage = \Drupal::service('locale.storage');
// If configuration contains a translation that is not in the po file then
// _install_config_locale_overrides_process_batch() will create a customized
// translation.
$translation = $locale_storage->findTranslation(['source' => 'Anonymous', 'language' => 'es']);
$this->assertSame('Anónimo', $translation->getString());
$this->assertTrue((bool) $translation->customized, "A customized translation has been created for Anonymous");
// If configuration contains a translation that is in the po file then
// _install_config_locale_overrides_process_batch() will not create a
// customized translation.
$translation = $locale_storage->findTranslation(['source' => 'Apply', 'language' => 'es']);
$this->assertSame('Aplicar', $translation->getString());
$this->assertFalse((bool) $translation->customized, "A non-customized translation has been created for Apply");
/** @var \Drupal\language\Config\LanguageConfigOverride $view_config */
// Ensure that views are translated as expected.
$view_config = \Drupal::languageManager()->getLanguageConfigOverride('es', 'views.view.who_s_new');
$this->assertSame('Aplicar', $view_config->get('display.default.display_options.exposed_form.options.submit_button'));
$view_config = \Drupal::languageManager()->getLanguageConfigOverride('es', 'views.view.archive');
$this->assertSame('Aplicar', $view_config->get('display.default.display_options.exposed_form.options.submit_button'));
// Manually update the translation status so can re-run the import.
$status = locale_translation_get_status();
$status['drupal']['es']->type = 'local';
$status['drupal']['es']->files['local']->timestamp = time();
\Drupal::keyValue('locale.translation_status')->set('drupal', $status['drupal']);
// Run the translation import.
$this->drupalGet('admin/reports/translations');
$this->submitForm([], 'Update translations');
// Ensure that only the config we expected to have changed has.
$comparer = $this->configImporter()->getStorageComparer();
$expected_changelist_spanish_collection = [
'create' => [],
// The view was untranslated but the translation exists so the installer
// performs the translation.
'update' => ['views.view.who_s_new'],
'delete' => [],
'rename' => [],
];
$this->assertEquals($expected_changelist_spanish_collection, $comparer->getChangelist(NULL, 'language.es'));
// Change a translation and ensure configuration is updated.
$po = <<<PO
msgid ""
msgstr ""
msgid "Anonymous"
msgstr "Anonymous es"
msgid "Apply"
msgstr "Aplicar New"
PO;
file_put_contents($this->publicFilesDirectory . '/translations/drupal-8.0.0.es.po', $po);
// Manually update the translation status so can re-run the import.
$status = locale_translation_get_status();
$status['drupal']['es']->type = 'local';
$status['drupal']['es']->files['local']->timestamp = time();
\Drupal::keyValue('locale.translation_status')->set('drupal', $status['drupal']);
// Run the translation import.
$this->drupalGet('admin/reports/translations');
$this->submitForm([], 'Update translations');
$translation = \Drupal::service('locale.storage')->findTranslation(['source' => 'Apply', 'language' => 'es']);
$this->assertSame('Aplicar New', $translation->getString());
$this->assertFalse((bool) $translation->customized, "A non-customized translation has been created for Apply");
// Ensure that only the config we expected to have changed has.
$comparer = $this->configImporter()->getStorageComparer();
$expected_changelist_spanish_collection = [
'create' => [],
// All views with 'Aplicar' will have been changed to use the new
// translation.
'update' => [
'views.view.archive',
'views.view.content_recent',
'views.view.frontpage',
'views.view.glossary',
'views.view.who_s_new',
'views.view.who_s_online',
],
'delete' => [],
'rename' => [],
];
$this->assertEquals($expected_changelist_spanish_collection, $comparer->getChangelist(NULL, 'language.es'));
}
/**
* Returns the string for the test .po file.
*
* @param string $langcode
* The language code.
*
* @return string
* Contents for the test .po file.
*/
protected function getPo($langcode): string {
return <<<PO
msgid ""
msgstr ""
msgid "Anonymous"
msgstr "Anonymous $langcode"
msgid "Apply"
msgstr "Aplicar"
PO;
}
}

View File

@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
/**
* Verifies that profiles with hook_install() can't be installed from config.
*
* @group Installer
*/
class InstallerExistingConfigSyncDirectoryProfileHookInstallTest extends InstallerConfigDirectoryTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected $profile = 'testing_config_install_multilingual';
/**
* {@inheritdoc}
*/
protected $existingSyncDirectory = TRUE;
/**
* {@inheritdoc}
*/
protected function visitInstaller(): void {
// Create an .install file with a hook_install() implementation.
$path = $this->siteDirectory . '/profiles/' . $this->profile;
$contents = <<<EOF
<?php
function testing_config_install_multilingual_install() {
}
EOF;
file_put_contents("$path/{$this->profile}.install", $contents);
parent::visitInstaller();
}
/**
* Installer step: Select installation profile.
*/
protected function setUpProfile(): void {
// This is the form we are testing so wait until the test method to do
// assertions.
}
/**
* Installer step: Requirements problem.
*/
protected function setUpRequirementsProblem(): void {
// This form will never be reached.
}
/**
* Installer step: Configure settings.
*/
protected function setUpSettings(): void {
// This form will never be reached.
}
/**
* Final installer step: Configure site.
*/
protected function setUpSite(): void {
// This form will never be reached.
}
/**
* {@inheritdoc}
*/
protected function getConfigLocation(): string {
return __DIR__ . '/../../../fixtures/config_install/multilingual';
}
/**
* Tests installing from config is not available due to hook_INSTALL().
*/
public function testConfigSync(): void {
$this->assertSession()->titleEquals('Select an installation profile | Drupal');
$this->assertSession()->responseNotContains('Use existing configuration');
// Remove the install hook and the option to install from existing
// configuration will be available.
unlink("{$this->siteDirectory}/profiles/{$this->profile}/{$this->profile}.install");
$this->getSession()->reload();
$this->assertSession()->titleEquals('Select an installation profile | Drupal');
$this->assertSession()->responseContains('Use existing configuration');
}
}

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
/**
* Verifies that installing from existing configuration works.
*
* @group Installer
*/
class InstallerExistingConfigSyncDirectoryProfileMismatchTest extends InstallerConfigDirectoryTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected $profile = 'testing_config_install_multilingual';
/**
* {@inheritdoc}
*/
protected $existingSyncDirectory = TRUE;
/**
* {@inheritdoc}
*/
protected function getConfigLocation(): string {
return __DIR__ . '/../../../fixtures/config_install/multilingual';
}
/**
* Installer step: Configure settings.
*/
protected function setUpSettings(): void {
// Cause a profile mismatch by hacking the URL.
$this->drupalGet(str_replace($this->profile, 'minimal', $this->getUrl()));
parent::setUpSettings();
}
protected function setUpSite(): void {
// This step will not occur because there is an error.
}
/**
* Tests that profile mismatch fails to install.
*/
public function testConfigSync(): void {
$this->htmlOutput(NULL);
$this->assertSession()->titleEquals('Configuration validation | Drupal');
$this->assertSession()->pageTextContains('The configuration synchronization failed validation.');
$this->assertSession()->pageTextContains('The selected installation profile minimal does not match the profile stored in configuration testing_config_install_multilingual.');
// Ensure there is no continuation button.
$this->assertSession()->pageTextNotContains('Save and continue');
$this->assertSession()->buttonNotExists('edit-submit');
}
}

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
// cspell:ignore nmsgid nmsgstr enregistrer
/**
* Verifies that installing from existing configuration works.
*
* @group Installer
*/
class InstallerExistingConfigTest extends InstallerConfigDirectoryTestBase {
/**
* {@inheritdoc}
*/
protected function setUpLanguage(): void {
// Place a custom local translation in the translations directory.
mkdir($this->root . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE);
file_put_contents($this->root . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.fr.po', "msgid \"\"\nmsgstr \"\"\nmsgid \"Save and continue\"\nmsgstr \"Enregistrer et continuer\"");
parent::setUpLanguage();
}
/**
* {@inheritdoc}
*/
public function setUpSettings(): void {
// The configuration is from a site installed in French.
// So after selecting the profile the installer detects that the site must
// be installed in French, thus we change the button translation.
$this->translations['Save and continue'] = 'Enregistrer et continuer';
parent::setUpSettings();
}
/**
* {@inheritdoc}
*/
protected function getConfigLocation(): string {
return __DIR__ . '/../../../fixtures/config_install/testing_config_install';
}
}

View File

@ -0,0 +1,179 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Archiver\ArchiveTar;
use Drupal\Core\Database\Database;
use Drupal\Core\Installer\Form\SelectProfileForm;
/**
* Provides a base class for testing installing from existing configuration.
*
* @deprecated in drupal:10.4.0 and is removed from drupal:12.0.0. Use
* \Drupal\FunctionalTests\Installer\InstallerConfigDirectoryTestBase
* instead.
*
* @see https://www.drupal.org/node/3460001
*/
abstract class InstallerExistingConfigTestBase extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $profile = NULL;
/**
* @var bool
*/
protected $existingSyncDirectory = FALSE;
/**
* {@inheritdoc}
*/
public function __construct(string $name) {
@trigger_error(__CLASS__ . ' is deprecated in drupal:10.4.0 and is removed from drupal:12.0.0. Use \Drupal\FunctionalTests\Installer\InstallerConfigDirectoryTestBase instead. See https://www.drupal.org/node/3460001');
parent::__construct($name);
}
/**
* {@inheritdoc}
*/
protected function prepareEnvironment() {
parent::prepareEnvironment();
$archiver = new ArchiveTar($this->getConfigTarball(), 'gz');
if ($this->profile === NULL) {
$core_extension = Yaml::decode($archiver->extractInString('core.extension.yml'));
$this->profile = $core_extension['profile'];
}
if ($this->profile !== FALSE) {
// Create a profile for testing. We set core_version_requirement to '*'
// for the test so that it does not need to be updated between major
// versions.
$info = [
'type' => 'profile',
'core_version_requirement' => '*',
'name' => 'Configuration installation test profile (' . $this->profile . ')',
];
// File API functions are not available yet.
$path = $this->siteDirectory . '/profiles/' . $this->profile;
// Put the sync directory inside the profile.
$config_sync_directory = $path . '/config/sync';
mkdir($path, 0777, TRUE);
file_put_contents("$path/{$this->profile}.info.yml", Yaml::encode($info));
}
else {
// If we have no profile we must use an existing sync directory.
$this->existingSyncDirectory = TRUE;
$config_sync_directory = $this->siteDirectory . '/config/sync';
}
if ($this->existingSyncDirectory) {
$config_sync_directory = $this->siteDirectory . '/config/sync';
$this->settings['settings']['config_sync_directory'] = (object) [
'value' => $config_sync_directory,
'required' => TRUE,
];
}
// Create config/sync directory and extract tarball contents to it.
mkdir($config_sync_directory, 0777, TRUE);
$files = [];
$list = $archiver->listContent();
if (is_array($list)) {
/** @var array $list */
foreach ($list as $file) {
$files[] = $file['filename'];
}
$archiver->extractList($files, $config_sync_directory);
}
// Add the module that is providing the database driver to the list of
// modules that can not be uninstalled in the core.extension configuration.
if (file_exists($config_sync_directory . '/core.extension.yml')) {
$core_extension = Yaml::decode(file_get_contents($config_sync_directory . '/core.extension.yml'));
$module = Database::getConnection()->getProvider();
if ($module !== 'core') {
$core_extension['module'][$module] = 0;
$core_extension['module'] = module_config_sort($core_extension['module']);
}
if ($this->profile === FALSE && array_key_exists('profile', $core_extension)) {
// Remove the profile.
unset($core_extension['module'][$core_extension['profile']]);
unset($core_extension['profile']);
// Set a default theme to the first theme that will be installed as this
// can not be retrieved from the profile.
$this->defaultTheme = array_key_first($core_extension['theme']);
}
file_put_contents($config_sync_directory . '/core.extension.yml', Yaml::encode($core_extension));
}
}
/**
* Gets the filepath to the configuration tarball.
*
* The tarball will be extracted to the install profile's config/sync
* directory for testing.
*
* @return string
* The filepath to the configuration tarball.
*/
abstract protected function getConfigTarball();
/**
* {@inheritdoc}
*/
protected function installParameters() {
$parameters = parent::installParameters();
// The options that change configuration are disabled when installing from
// existing configuration.
unset($parameters['forms']['install_configure_form']['site_name']);
unset($parameters['forms']['install_configure_form']['site_mail']);
unset($parameters['forms']['install_configure_form']['enable_update_status_module']);
unset($parameters['forms']['install_configure_form']['enable_update_status_emails']);
return $parameters;
}
/**
* Confirms that the installation installed the configuration correctly.
*/
public function testConfigSync(): void {
// After installation there is no snapshot and nothing to import.
$change_list = $this->configImporter()->getStorageComparer()->getChangelist();
$expected = [
'create' => [],
// The system.mail is changed configuration because the test system
// changes it to ensure that mails are not sent.
'update' => ['system.mail'],
'delete' => [],
'rename' => [],
];
$this->assertEquals($expected, $change_list);
}
/**
* Installer step: Select installation profile.
*/
protected function setUpProfile() {
if ($this->existingSyncDirectory) {
$edit = [
'profile' => SelectProfileForm::CONFIG_INSTALL_PROFILE_KEY,
];
$this->submitForm($edit, $this->translations['Save and continue']);
}
else {
parent::setUpProfile();
}
}
}

View File

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Database\Database;
/**
* Tests installation with database information in an existing settings file.
*
* @group Installer
*/
class InstallerExistingDatabaseSettingsTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function prepareEnvironment(): void {
parent::prepareEnvironment();
// Pre-configure database credentials in settings.php.
$connection_info = Database::getConnectionInfo();
unset($connection_info['default']['pdo']);
unset($connection_info['default']['init_commands']);
$this->settings['databases']['default'] = (object) [
'value' => $connection_info,
'required' => TRUE,
];
}
/**
* {@inheritdoc}
*
* @todo The database settings form is not supposed to appear if settings.php
* contains a valid database connection already (but e.g. no config
* directories yet).
*/
protected function setUpSettings(): void {
// All database settings should be pre-configured, except password.
$values = $this->parameters['forms']['install_settings_form'];
$driver = $values['driver'];
$edit = [];
if (isset($values[$driver]['password']) && $values[$driver]['password'] !== '') {
$edit = $this->translatePostValues([
$driver => [
'password' => $values[$driver]['password'],
],
]);
}
$this->submitForm($edit, $this->translations['Save and continue']);
}
/**
* Verifies that installation succeeded.
*/
public function testInstaller(): void {
$this->assertSession()->addressEquals('user/1');
$this->assertSession()->statusCodeEquals(200);
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
/**
* Tests the installer with an existing Drupal installation.
*
* @group Installer
*/
class InstallerExistingInstallationTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests that Drupal fails to install when there is an existing installation.
*/
public function testInstaller(): void {
// Verify that Drupal can't be immediately reinstalled.
$this->visitInstaller();
$this->assertSession()->pageTextContains('Drupal already installed');
// Verify that Drupal version is not displayed.
$this->assertSession()->pageTextNotContains(\Drupal::VERSION);
// Delete settings.php and attempt to reinstall again.
unlink($this->siteDirectory . '/settings.php');
$this->visitInstaller();
$this->setUpLanguage();
$this->setUpProfile();
$this->setUpRequirementsProblem();
$this->setUpSettings();
$this->assertSession()->pageTextContains('Drupal already installed');
}
}

View File

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Database\Database;
use Symfony\Component\HttpFoundation\Request;
/**
* Tests the installer with an existing settings file but no install profile.
*
* @group Installer
*/
class InstallerExistingSettingsNoProfileTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*
* Configures a preexisting settings.php file without an install_profile
* setting before invoking the interactive installer.
*/
protected function prepareEnvironment(): void {
parent::prepareEnvironment();
// Pre-configure hash salt.
// Any string is valid, so simply use the class name of this test.
$this->settings['settings']['hash_salt'] = (object) [
'value' => __CLASS__,
'required' => TRUE,
];
// Pre-configure database credentials.
$connection_info = Database::getConnectionInfo();
unset($connection_info['default']['pdo']);
unset($connection_info['default']['init_commands']);
$this->settings['databases']['default'] = (object) [
'value' => $connection_info,
'required' => TRUE,
];
// Pre-configure config directories.
$this->settings['settings']['config_sync_directory'] = (object) [
'value' => DrupalKernel::findSitePath(Request::createFromGlobals()) . '/files/config_sync',
'required' => TRUE,
];
mkdir($this->settings['settings']['config_sync_directory']->value, 0777, TRUE);
}
/**
* {@inheritdoc}
*/
protected function setUpSettings(): void {
// This step should not appear, since settings.php is fully configured
// already.
}
/**
* Verifies that installation succeeded.
*/
public function testInstaller(): void {
$this->assertSession()->addressEquals('user/1');
$this->assertSession()->statusCodeEquals(200);
$this->assertEquals('testing', \Drupal::installProfile());
}
}

View File

@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Database\Database;
use Drupal\Core\DrupalKernel;
use Symfony\Component\HttpFoundation\Request;
/**
* Tests the installer with an existing settings file.
*
* @group Installer
*/
class InstallerExistingSettingsTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*
* Fully configures a preexisting settings.php file before invoking the
* interactive installer.
*/
protected function prepareEnvironment(): void {
parent::prepareEnvironment();
// Pre-configure hash salt.
// Any string is valid, so simply use the class name of this test.
$this->settings['settings']['hash_salt'] = (object) [
'value' => __CLASS__,
'required' => TRUE,
];
// Pre-configure database credentials.
$connection_info = Database::getConnectionInfo();
unset($connection_info['default']['pdo']);
unset($connection_info['default']['init_commands']);
$this->settings['databases']['default'] = (object) [
'value' => $connection_info,
'required' => TRUE,
];
// Use the kernel to find the site path because the site.path service should
// not be available at this point in the install process.
$site_path = DrupalKernel::findSitePath(Request::createFromGlobals());
// Pre-configure config directories.
$this->settings['settings']['config_sync_directory'] = (object) [
'value' => $site_path . '/files/config_sync',
'required' => TRUE,
];
mkdir($this->settings['settings']['config_sync_directory']->value, 0777, TRUE);
}
/**
* Visits the interactive installer.
*/
protected function visitInstaller(): void {
// Should redirect to the installer.
$this->drupalGet($GLOBALS['base_url']);
// Ensure no database tables have been created yet.
$this->assertSame([], Database::getConnection()->schema()->findTables('%'));
$this->assertSession()->addressEquals($GLOBALS['base_url'] . '/core/install.php');
}
/**
* {@inheritdoc}
*/
protected function setUpSettings(): void {
// This step should not appear, since settings.php is fully configured
// already.
}
/**
* Verifies that installation succeeded.
*/
public function testInstaller(): void {
$this->assertSession()->addressEquals('user/1');
$this->assertSession()->statusCodeEquals(200);
$this->assertEquals('testing', \Drupal::installProfile());
}
}

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
// cspell:ignore nmsgid nmsgstr
/**
* Verifies that the early installer uses the correct language direction.
*
* @group Installer
*/
class InstallerLanguageDirectionTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Overrides the language code the installer should use.
*
* @var string
*/
protected $langcode = 'ar';
/**
* {@inheritdoc}
*/
protected function setUpLanguage(): void {
// Place a custom local translation in the translations directory.
mkdir($this->root . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE);
file_put_contents($this->root . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.ar.po', "msgid \"\"\nmsgstr \"\"\nmsgid \"Save and continue\"\nmsgstr \"Save and continue Arabic\"");
parent::setUpLanguage();
// After selecting a different language than English, all following screens
// should be translated already.
$this->assertSession()->buttonExists('Save and continue Arabic');
$this->translations['Save and continue'] = 'Save and continue Arabic';
// Verify that language direction is right-to-left.
$this->assertSession()->elementTextEquals('xpath', '/@dir', 'rtl');
}
/**
* Confirms that the installation succeeded.
*/
public function testInstalled(): void {
$this->assertSession()->addressEquals('user/1');
$this->assertSession()->statusCodeEquals(200);
}
}

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Language\LanguageManager;
// cspell:ignore xoxo
/**
* Verifies that the installer language list uses local and remote languages.
*
* @group Installer
*/
class InstallerLanguagePageTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Installer step: Select language.
*/
protected function setUpLanguage(): void {
// Place a custom local translation in the translations directory.
mkdir($this->root . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE);
touch($this->root . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.xoxo.po');
// Check that all predefined languages show up with their native names.
$this->visitInstaller();
foreach (LanguageManager::getStandardLanguageList() as $langcode => $names) {
$this->assertSession()->optionExists('edit-langcode', $langcode);
$this->assertSession()->responseContains('>' . $names[1] . '<');
}
// Check that our custom one shows up with the file name indicated language.
$this->assertSession()->optionExists('edit-langcode', 'xoxo');
$this->assertSession()->responseContains('>xoxo<');
parent::setUpLanguage();
}
/**
* Confirms that the installation succeeded.
*/
public function testInstalled(): void {
$this->assertSession()->addressEquals('user/1');
$this->assertSession()->statusCodeEquals(200);
}
}

View File

@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Database\Database;
// cspell:ignore drupaldriver testdriverdatabasedrivertestmysql
// cspell:ignore testdriverdatabasedrivertestpgsql
/**
* Tests the interactive installer.
*
* @group Installer
*/
class InstallerNonDefaultDatabaseDriverTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The name of the test database driver in use.
*
* @var string
*/
protected $testDriverName;
/**
* {@inheritdoc}
*/
protected function setUpSettings(): void {
$driver = Database::getConnection()->driver();
if (!in_array($driver, ['mysql', 'pgsql'])) {
$this->markTestSkipped("This test does not support the {$driver} database driver.");
}
$driverNamespace = Database::getConnection()->getConnectionOptions()['namespace'];
$this->testDriverName = 'DriverTest' . ucfirst($driver);
$testDriverNamespace = "Drupal\\driver_test\\Driver\\Database\\{$this->testDriverName}";
// Assert that we are using the database drivers from the driver_test
// module.
$this->assertSession()->elementTextEquals('xpath', '//label[@for="edit-driver-drupaldriver-testdriverdatabasedrivertestmysql"]', 'MySQL by the driver_test module');
$this->assertSession()->elementTextEquals('xpath', '//label[@for="edit-driver-drupaldriver-testdriverdatabasedrivertestpgsql"]', 'PostgreSQL by the driver_test module');
$settings = $this->parameters['forms']['install_settings_form'];
$settings['driver'] = $testDriverNamespace;
$settings[$testDriverNamespace] = $settings[$driverNamespace];
unset($settings[$driverNamespace]);
$edit = $this->translatePostValues($settings);
$this->submitForm($edit, $this->translations['Save and continue']);
}
/**
* Confirms that the installation succeeded.
*/
public function testInstalled(): void {
$this->assertSession()->addressEquals('user/1');
$this->assertSession()->statusCodeEquals(200);
// Assert that in the settings.php the database connection array has the
// correct values set.
$installedDatabaseSettings = $this->getInstalledDatabaseSettings();
$this->assertSame("Drupal\\driver_test\\Driver\\Database\\{$this->testDriverName}", $installedDatabaseSettings['default']['default']['namespace']);
$this->assertSame($this->testDriverName, $installedDatabaseSettings['default']['default']['driver']);
$this->assertSame("core/modules/system/tests/modules/driver_test/src/Driver/Database/{$this->testDriverName}/", $installedDatabaseSettings['default']['default']['autoload']);
$this->assertEquals([
'mysql' => [
'namespace' => 'Drupal\\mysql',
'autoload' => 'core/modules/mysql/src/',
],
'pgsql' => [
'namespace' => 'Drupal\\pgsql',
'autoload' => 'core/modules/pgsql/src/',
],
], $installedDatabaseSettings['default']['default']['dependencies']);
// Assert that the module "driver_test" and its dependencies have been
// installed.
$this->drupalGet('admin/modules');
$this->assertSession()->checkboxChecked('modules[driver_test][enable]');
$this->assertSession()->checkboxChecked('modules[mysql][enable]');
$this->assertSession()->checkboxChecked('modules[pgsql][enable]');
// The module "driver_test" can not be uninstalled, because it is providing
// the database driver. Also, the "mysql" and "pgsql" modules can not be
// uninstalled being dependencies of the "driver_test" module.
$this->drupalGet('admin/modules/uninstall');
$this->assertSession()->elementTextContains('xpath', '//tr[@data-drupal-selector="edit-driver-test"]', "The following reason prevents Contrib database driver test from being uninstalled: The module 'Contrib database driver test' is providing the database driver '{$this->testDriverName}'.");
$this->assertSession()->elementTextContains('xpath', '//tr[@data-drupal-selector="edit-mysql"]', "The following reason prevents MySQL from being uninstalled: Required by: driver_test");
$this->assertSession()->elementTextContains('xpath', '//tr[@data-drupal-selector="edit-pgsql"]', "The following reason prevents PostgreSQL from being uninstalled: Required by: driver_test");
}
/**
* Returns the databases setup from the SUT's settings.php.
*
* @return array<string,mixed>
* The value of the $databases variable.
*/
protected function getInstalledDatabaseSettings(): array {
// The $app_root and $site_path variables are required by the settings.php
// file to be parsed correctly. The $databases variable is set in the
// included file, we need to inform PHPStan about that since PHPStan itself
// is unable to determine it.
$app_root = $this->container->getParameter('app.root');
$site_path = $this->siteDirectory;
include $app_root . '/' . $site_path . '/settings.php';
assert(isset($databases));
return $databases;
}
}

View File

@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Serialization\Yaml;
/**
* Tests installing a profile with non-English language and no locale module.
*
* @group Installer
*/
class InstallerNonEnglishProfileWithoutLocaleModuleTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The testing profile name.
*
* @var string
*/
const PROFILE = 'testing_with_language_without_locale';
/**
* {@inheritdoc}
*/
protected $profile = self::PROFILE;
/**
* {@inheritdoc}
*/
protected function prepareEnvironment(): void {
parent::prepareEnvironment();
// Create a self::PROFILE testing profile that depends on the 'language'
// module but not on 'locale' module. We set core_version_requirement to '*'
// for the test so that it does not need to be updated between major
// versions.
$profile_info = [
'type' => 'profile',
'core_version_requirement' => '*',
'name' => 'Test with language but without locale',
'install' => ['language'],
];
// File API functions are not available yet.
$profile_dir = "{$this->root}/{$this->siteDirectory}/profiles/" . self::PROFILE;
$profile_config_dir = "$profile_dir/config/install";
mkdir($profile_config_dir, 0777, TRUE);
$profile_info_file = $profile_dir . '/' . static::PROFILE . '.info.yml';
file_put_contents($profile_info_file, Yaml::encode($profile_info));
// Copy a non-English language config YAML to be installed with the profile.
copy($this->root . '/core/profiles/tests/testing_multilingual/config/install/language.entity.de.yml', $profile_config_dir . '/language.entity.de.yml');
}
/**
* Tests installing a profile with non-English language and no locale module.
*/
public function testNonEnglishProfileWithoutLocaleModule(): void {
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->addressEquals('user/1');
// Confirm that we are logged-in after installation.
$this->assertSession()->pageTextContains($this->rootUser->getAccountName());
// Verify that the confirmation message appears.
require_once $this->root . '/core/includes/install.inc';
$this->assertSession()->pageTextContains('Congratulations, you installed Drupal!');
$this->assertFalse(\Drupal::service('module_handler')->moduleExists('locale'), 'The Locale module is not installed.');
$this->assertTrue(\Drupal::service('module_handler')->moduleExists('language'), 'The Language module is installed.');
$this->assertTrue(\Drupal::languageManager()->isMultilingual(), 'The language manager is multi-lingual.');
}
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Routing\RoutingEvents;
use Drupal\Core\Test\PerformanceTestRecorder;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the interactive installer.
*
* @group Installer
*/
class InstallerPerformanceTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected $profile = 'testing';
/**
* {@inheritdoc}
*/
protected function prepareSettings(): void {
parent::prepareSettings();
PerformanceTestRecorder::registerService($this->siteDirectory . '/services.yml', FALSE);
}
/**
* Ensures that the user page is available after installation.
*/
public function testInstaller(): void {
// Ensures that router is not rebuilt unnecessarily during the install.
// Currently it is built once during the install in install_finished().
$this->assertSame(1, \Drupal::service('core.performance.test.recorder')->getCount('event', RoutingEvents::FINISHED));
}
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
/**
* Tests re-visiting the installer after a successful installation.
*
* @group Installer
*/
class InstallerPostInstallTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $profile = 'minimal';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Confirms that visiting the installer does not break things post-install.
*/
public function testVisitInstallerPostInstall(): void {
\Drupal::service('module_installer')->install(['system_test']);
// Clear caches to ensure that system_test's routes are available.
$this->resetAll();
// Confirm that the install_profile is correct.
$this->drupalGet('/system-test/get-install-profile');
$this->assertSession()->pageTextContains('minimal');
// Make an anonymous visit to the installer
$this->drupalLogout();
$this->visitInstaller();
// Ensure that the install profile is still correct.
$this->drupalGet('/system-test/get-install-profile');
$this->assertSession()->pageTextContains('minimal');
}
}

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
/**
* Tests that an install profile can implement hook_requirements().
*
* @group Installer
*/
class InstallerProfileRequirementsTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected $profile = 'testing_requirements';
/**
* {@inheritdoc}
*/
protected function setUpSettings(): void {
// This form will never be reached.
}
/**
* {@inheritdoc}
*/
protected function setUpRequirementsProblem(): void {
// The parent method asserts that there are no requirements errors, but
// this test expects a requirements error in the test method below.
// Therefore, we override this method to suppress the parent's assertions.
}
/**
* {@inheritdoc}
*/
protected function setUpSite(): void {
// This form will never be reached.
}
/**
* Assert that the profile failed hook_requirements().
*/
public function testHookRequirementsFailure(): void {
$this->assertSession()->pageTextContains('Testing requirements failed requirements.');
}
}

View File

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Routing\RoutingEvents;
use Drupal\Core\Serialization\Yaml;
use Drupal\Core\Test\PerformanceTestRecorder;
/**
* Tests router rebuilding during installation.
*
* @group Installer
*/
class InstallerRouterTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $profile = 'test_profile';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function prepareEnvironment(): void {
parent::prepareEnvironment();
$info = [
'type' => 'profile',
'core_version_requirement' => '*',
'name' => 'Router testing profile',
'install' => [
'router_test',
'router_installer_test',
],
];
// File API functions are not available yet.
$path = $this->siteDirectory . '/profiles/test_profile';
mkdir($path, 0777, TRUE);
file_put_contents("$path/test_profile.info.yml", Yaml::encode($info));
$settings_services_file = DRUPAL_ROOT . '/sites/default/default.services.yml';
copy($settings_services_file, $this->siteDirectory . '/services.yml');
PerformanceTestRecorder::registerService($this->siteDirectory . '/services.yml', TRUE);
}
/**
* Confirms that the installation succeeded.
*/
public function testInstalled(): void {
$this->assertSession()->statusCodeEquals(200);
// Ensures that router is not rebuilt unnecessarily during the install. It
// is rebuilt during:
// - router_test_install()
// - router_installer_test_modules_installed()
// - install_finished()
$this->assertSame(3, \Drupal::service('core.performance.test.recorder')->getCount('event', RoutingEvents::FINISHED));
$this->assertStringEndsWith('/core/install.php/router_installer_test/test1', \Drupal::state()->get('router_installer_test_modules_installed'));
$this->assertStringEndsWith('/core/install.php/router_test/test1', \Drupal::state()->get('router_test_install'));
}
}

View File

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
/**
* Verifies that the installer uses the profile's site configuration.
*
* @group Installer
*/
class InstallerSiteConfigProfileTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected $profile = 'testing_site_config';
/**
* The site mail we expect to be set from the install profile.
*
* @see testing_site_config_install()
*/
const EXPECTED_SITE_MAIL = 'profile-testing-site-config@example.com';
/**
* The timezone we expect to be set from the install profile.
*
* @see testing_site_config_install()
*/
const EXPECTED_TIMEZONE = 'America/Los_Angeles';
/**
* {@inheritdoc}
*/
protected function installParameters() {
$parameters = parent::installParameters();
// Don't override the site email address, allowing it to default to the one
// from our install profile.
unset($parameters['forms']['install_configure_form']['site_mail']);
return $parameters;
}
/**
* {@inheritdoc}
*/
protected function setUpSite() {
$this->assertSession()->fieldValueEquals('site_mail', self::EXPECTED_SITE_MAIL);
$this->assertSession()->fieldValueEquals('date_default_timezone', self::EXPECTED_TIMEZONE);
return parent::setUpSite();
}
/**
* Verify the correct site config was set.
*/
public function testInstaller(): void {
$this->assertEquals(self::EXPECTED_SITE_MAIL, $this->config('system.site')->get('mail'));
$this->assertEquals(self::EXPECTED_TIMEZONE, $this->config('system.date')->get('timezone.default'));
}
}

View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
/**
* Verifies that the installer skipped permission hardening.
*
* @group Installer
*/
class InstallerSkipPermissionHardeningTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function prepareEnvironment(): void {
parent::prepareEnvironment();
$this->settings['settings']['skip_permissions_hardening'] = (object) ['value' => TRUE, 'required' => TRUE];
}
/**
* {@inheritdoc}
*/
protected function setUpSite(): void {
$site_directory = $this->container->getParameter('app.root') . '/' . $this->siteDirectory;
$this->assertDirectoryIsWritable($site_directory);
$this->assertFileIsWritable($site_directory . '/settings.php');
$this->assertSession()->responseContains('All necessary changes to <em class="placeholder">' . $this->siteDirectory . '</em> and <em class="placeholder">' . $this->siteDirectory . '/settings.php</em> have been made, so you should remove write permissions to them now in order to avoid security risks. If you are unsure how to do so, consult the <a href="https://www.drupal.org/server-permissions">online handbook</a>.');
parent::setUpSite();
}
/**
* Verifies the expected behaviors of the installation result.
*/
public function testInstalled(): void {
$this->assertSession()->addressEquals('user/1');
$this->assertSession()->statusCodeEquals(200);
}
}

View File

@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Database\Database;
use Drupal\Core\Routing\RoutingEvents;
use Drupal\Core\Test\PerformanceTestRecorder;
use Drupal\Core\Extension\ModuleUninstallValidatorException;
// cspell:ignore drupalmysqldriverdatabasemysql drupalpgsqldriverdatabasepgsql
/**
* Tests the interactive installer.
*
* @group Installer
*/
class InstallerTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Ensures that the user page is available after installation.
*/
public function testInstaller(): void {
$this->assertNotEquals('0', \Drupal::service('asset.query_string')->get(), 'The dummy query string should be set during install');
$this->assertSession()->addressEquals('user/1');
$this->assertSession()->statusCodeEquals(200);
// Confirm that we are logged-in after installation.
$this->assertSession()->pageTextContains($this->rootUser->getAccountName());
// Verify that the confirmation message appears.
require_once $this->root . '/core/includes/install.inc';
$this->assertSession()->pageTextContains('Congratulations, you installed Drupal!');
// Ensure that the timezone is correct for sites under test after installing
// interactively.
$this->assertEquals('Australia/Sydney', $this->config('system.date')->get('timezone.default'));
// Ensure the profile has a weight of 1000.
$module_extension_list = \Drupal::service('extension.list.module');
$extensions = $module_extension_list->getList();
$this->assertArrayHasKey('testing', $extensions);
$this->assertEquals(1000, $extensions['testing']->weight);
// Ensures that router is not rebuilt unnecessarily during the install.
$this->assertSame(1, \Drupal::service('core.performance.test.recorder')->getCount('event', RoutingEvents::FINISHED));
}
/**
* Installer step: Select language.
*/
protected function setUpLanguage(): void {
// Test that \Drupal\Core\Render\BareHtmlPageRenderer adds assets and
// metatags as expected to the first page of the installer.
$this->assertSession()->responseContains("css/components/button.css");
$this->assertSession()->responseContains('<meta charset="utf-8" />');
// Assert that the expected title is present.
$this->assertEquals('Choose language', $this->cssSelect('main h2')[0]->getText());
parent::setUpLanguage();
}
/**
* {@inheritdoc}
*/
protected function setUpProfile(): void {
$settings_services_file = DRUPAL_ROOT . '/sites/default/default.services.yml';
// Copy the testing-specific service overrides in place.
copy($settings_services_file, $this->siteDirectory . '/services.yml');
PerformanceTestRecorder::registerService($this->siteDirectory . '/services.yml', TRUE);
// Assert that the expected title is present.
$this->assertEquals('Select an installation profile', $this->cssSelect('main h2')[0]->getText());
// Verify that Title/Label are not displayed when '#title_display' =>
// 'invisible' attribute is set.
$this->assertSession()->elementsCount('xpath', "//span[contains(@class, 'visually-hidden') and contains(text(), 'Select an installation profile')]", 1);
parent::setUpProfile();
}
/**
* {@inheritdoc}
*/
protected function setUpSettings(): void {
// Assert that the expected title is present.
$this->assertEquals('Database configuration', $this->cssSelect('main h2')[0]->getText());
// Assert that we use the by core supported database drivers by default and
// not the ones from the driver_test module.
$this->assertSession()->elementTextEquals('xpath', '//label[@for="edit-driver-drupalmysqldriverdatabasemysql"]', 'MySQL, MariaDB, Percona Server, or equivalent');
$this->assertSession()->elementTextEquals('xpath', '//label[@for="edit-driver-drupalpgsqldriverdatabasepgsql"]', 'PostgreSQL');
parent::setUpSettings();
}
/**
* {@inheritdoc}
*/
protected function setUpSite(): void {
// Assert that the expected title is present.
$this->assertEquals('Configure site', $this->cssSelect('main h2')[0]->getText());
// Test that SiteConfigureForm::buildForm() has made the site directory and
// the settings file non-writable.
$site_directory = $this->container->getParameter('app.root') . '/' . $this->siteDirectory;
$this->assertDirectoryIsNotWritable($site_directory);
$this->assertFileIsNotWritable($site_directory . '/settings.php');
parent::setUpSite();
}
/**
* {@inheritdoc}
*/
protected function visitInstaller(): void {
parent::visitInstaller();
// Assert the title is correct and has the title suffix.
$this->assertSession()->titleEquals('Choose language | Drupal');
}
/**
* Confirms that the installation succeeded.
*/
public function testInstalled(): void {
$this->assertSession()->addressEquals('user/1');
$this->assertSession()->statusCodeEquals(200);
$database = Database::getConnection();
$module = $database->getProvider();
$module_handler = \Drupal::service('module_handler');
$module_extension_list = \Drupal::service('extension.list.module');
// Ensure the Update Status module is not installed.
$this->assertFalse($module_handler->moduleExists('update'), 'The Update Status module should not be installed.');
// Assert that the module that is providing the database driver has been
// installed.
$this->assertTrue($module_handler->moduleExists($module));
// The module that is providing the database driver should be uninstallable.
try {
$this->container->get('module_installer')->uninstall([$module]);
$this->fail("Uninstalled $module module.");
}
catch (ModuleUninstallValidatorException $e) {
$module_name = $module_extension_list->getName($module);
$driver = $database->driver();
$this->assertStringContainsString("The module '$module_name' is providing the database driver '$driver'.", $e->getMessage());
}
}
}

View File

@ -0,0 +1,305 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Language\Language;
use Drupal\Core\Session\UserSession;
use Drupal\Core\Site\Settings;
use Drupal\Core\Test\HttpClientMiddleware\TestHttpClientMiddleware;
use Drupal\Core\Utility\PhpRequirements;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\RequirementsPageTrait;
use GuzzleHttp\HandlerStack;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
/**
* Base class for testing the interactive installer.
*/
abstract class InstallerTestBase extends BrowserTestBase {
use RequirementsPageTrait;
/**
* Custom settings.php values to write for a test run.
*
* @var array
* An array of settings to write out, in the format expected by
* SettingsEditor::rewrite().
*
* @see \Drupal\Core\Site\SettingsEditor::rewrite()
*/
protected $settings = [];
/**
* The language code in which to install Drupal.
*
* @var string
*/
protected $langcode = 'en';
/**
* The installation profile to install.
*
* @var string
*/
protected $profile = 'testing';
/**
* Additional parameters to use for installer screens.
*
* @var array
*
* @see FunctionalTestSetupTrait::installParameters()
*/
protected $parameters = [];
/**
* A string translation map used for translated installer screens.
*
* Keys are English strings, values are translated strings.
*
* @var array
*/
protected $translations = [
'Save and continue' => 'Save and continue',
];
/**
* Whether the installer has completed.
*
* @var bool
*/
protected $isInstalled = FALSE;
/**
* {@inheritdoc}
*/
protected function installParameters() {
$params = parent::installParameters();
// Set the checkbox values to FALSE so that
// \Drupal\Tests\BrowserTestBase::translatePostValues() does not remove
// them.
$params['forms']['install_configure_form']['enable_update_status_module'] = FALSE;
$params['forms']['install_configure_form']['enable_update_status_emails'] = FALSE;
return $params;
}
/**
* We are testing the installer, so set up a minimal environment for that.
*/
public function installDrupal() {
// Define information about the user 1 account.
$this->rootUser = new UserSession([
'uid' => 1,
'name' => 'admin',
'mail' => 'admin@example.com',
'pass_raw' => $this->randomMachineName(),
]);
// If any $settings are defined for this test, copy and prepare an actual
// settings.php, so as to resemble a regular installation.
if (!empty($this->settings)) {
// Not using File API; a potential error must trigger a PHP warning.
copy(DRUPAL_ROOT . '/sites/default/default.settings.php', DRUPAL_ROOT . '/' . $this->siteDirectory . '/settings.php');
$this->writeSettings($this->settings);
}
// Note that FunctionalTestSetupTrait::installParameters() returns form
// input values suitable for a programmed
// \Drupal::formBuilder()->submitForm().
// @see InstallerTestBase::translatePostValues()
$this->parameters = $this->installParameters();
// Set up a minimal container (required by BrowserTestBase). Set cookie and
// server information so that XDebug works.
// @see install_begin_request()
$request = Request::create($GLOBALS['base_url'] . '/core/install.php', 'GET', [], $_COOKIE, [], $_SERVER);
$request->setSession(new Session(new MockArraySessionStorage()));
$this->container = new ContainerBuilder();
$request_stack = new RequestStack();
$request_stack->push($request);
$this->container
->set('request_stack', $request_stack);
$this->container
->setParameter('language.default_values', Language::$defaultValues);
$this->container
->register('language.default', 'Drupal\Core\Language\LanguageDefault')
->addArgument('%language.default_values%');
$this->container
->register('string_translation', 'Drupal\Core\StringTranslation\TranslationManager')
->addArgument(new Reference('language.default'));
$this->container
->register('http_client', 'GuzzleHttp\Client')
->setFactory('http_client_factory:fromOptions');
$this->container
->register('http_client_factory', 'Drupal\Core\Http\ClientFactory')
->setArguments([new Reference('http_handler_stack')]);
$handler_stack = HandlerStack::create();
$test_http_client_middleware = new TestHttpClientMiddleware();
$handler_stack->push($test_http_client_middleware(), 'test.http_client.middleware');
$this->container
->set('http_handler_stack', $handler_stack);
$this->container
->setParameter('app.root', DRUPAL_ROOT);
\Drupal::setContainer($this->container);
}
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->visitInstaller();
// Select language.
$this->setUpLanguage();
// Select profile.
$this->setUpProfile();
// Address the requirements problem screen, if any.
$this->setUpRequirementsProblem();
// Configure settings.
$this->setUpSettings();
// @todo Allow test classes based on this class to act on further installer
// screens.
// Configure site.
$this->setUpSite();
if ($this->isInstalled) {
// Import new settings.php written by the installer.
$request = Request::createFromGlobals();
$class_loader = require $this->container->getParameter('app.root') . '/autoload.php';
Settings::initialize($this->container->getParameter('app.root'), DrupalKernel::findSitePath($request), $class_loader);
// After writing settings.php, the installer removes write permissions
// from the site directory. To allow drupal_generate_test_ua() to write
// a file containing the private key for drupal_valid_test_ua(), the site
// directory has to be writable.
// BrowserTestBase::tearDown() will delete the entire test site directory.
// Not using File API; a potential error must trigger a PHP warning.
chmod($this->container->getParameter('app.root') . '/' . $this->siteDirectory, 0777);
$this->kernel = DrupalKernel::createFromRequest($request, $class_loader, 'prod', FALSE);
$this->kernel->boot();
$this->kernel->preHandle($request);
$this->container = $this->kernel->getContainer();
// Manually configure the test mail collector implementation to prevent
// tests from sending out emails and collect them in state instead.
$this->container->get('config.factory')
->getEditable('system.mail')
->set('interface.default', 'test_mail_collector')
->set('mailer_dsn', [
'scheme' => 'null',
'host' => 'null',
'user' => NULL,
'password' => NULL,
'port' => NULL,
'options' => [],
])
->save();
$this->installDefaultThemeFromClassProperty($this->container);
}
}
/**
* {@inheritdoc}
*/
protected function initFrontPage() {
// We don't want to visit the front page with the installer when
// initializing Mink, so we do nothing here.
}
/**
* Visits the interactive installer.
*/
protected function visitInstaller() {
$this->drupalGet($GLOBALS['base_url'] . '/core/install.php');
}
/**
* Installer step: Select language.
*
* @see \Drupal\Core\Installer\Form\SelectLanguageForm
*/
protected function setUpLanguage() {
$edit = [
'langcode' => $this->langcode,
];
// The 'Select Language' step is always English.
$this->submitForm($edit, 'Save and continue');
}
/**
* Installer step: Select installation profile.
*/
protected function setUpProfile() {
$edit = [
'profile' => $this->profile,
];
$this->submitForm($edit, $this->translations['Save and continue']);
}
/**
* Installer step: Configure settings.
*/
protected function setUpSettings() {
$parameters = $this->parameters['forms']['install_settings_form'];
$driver = $parameters['driver'];
unset($parameters[$driver]['dependencies']);
$edit = $this->translatePostValues($parameters);
$this->submitForm($edit, $this->translations['Save and continue']);
}
/**
* Installer step: Requirements problem.
*
* Override this method to test specific requirements warnings or errors
* during the installer.
*
* @see system_requirements()
*/
protected function setUpRequirementsProblem() {
if (version_compare(phpversion(), PhpRequirements::getMinimumSupportedPhp()) < 0) {
$this->continueOnExpectedWarnings(['PHP']);
}
}
/**
* Final installer step: Configure site.
*/
protected function setUpSite() {
$edit = $this->translatePostValues($this->parameters['forms']['install_configure_form']);
$this->submitForm($edit, $this->translations['Save and continue']);
// If we've got to this point the site is installed using the regular
// installation workflow.
$this->isInstalled = TRUE;
}
/**
* {@inheritdoc}
*
* FunctionalTestSetupTrait::refreshVariables() tries to operate on persistent
* storage, which is only available after the installer completed.
*/
protected function refreshVariables() {
if ($this->isInstalled) {
parent::refreshVariables();
}
}
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\block\Entity\Block;
/**
* Verifies that the installer does not generate theme blocks.
*
* @group Installer
*/
class InstallerThemesBlocksProfileTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'testing_theme_required_blocks';
/**
* {@inheritdoc}
*/
protected $profile = 'testing_themes_blocks';
/**
* Verify that there is no automatic block generation.
*/
public function testInstaller(): void {
// Account menu is a block that testing_theme_required_blocks provides,
// but not testing_theme_optional_blocks. There shouldn't be a account menu
// block for testing_theme_optional_blocks after the installation.
$this->assertEmpty(Block::load('testing_theme_optional_blocks_account_menu'));
$this->assertNotEmpty(Block::load('testing_theme_optional_blocks_page_title'));
// Ensure that for themes without blocks, some default blocks will be
// created.
$this->assertNotEmpty(Block::load('testing_theme_without_blocks_account_menu'));
}
}

View File

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
/**
* Tests translation files for multiple languages get imported during install.
*
* @group Installer
*/
class InstallerTranslationExistingFileTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Overrides the language code in which to install Drupal.
*
* Choose one of the smaller languages on ftp.drupal.org. There is no way to
* avoid using ftp.drupal.org since the code being tested runs extremely early
* in the installer. However, even if the call to ftp.drupal.org fails then
* this test will not fail as it will end up on the requirements page.
*
* @var string
*/
protected $langcode = 'xx-lolspeak';
/**
* {@inheritdoc}
*/
protected function setUpLanguage(): void {
// Place custom local translations in the translations directory.
mkdir(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE);
$po_contents = <<<PO
msgid ""
msgstr ""
PO;
// Create a misnamed translation file that
// \Drupal\Core\StringTranslation\Translator\FileTranslation::findTranslationFiles()
// will not find.
file_put_contents(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0-DEV.xx-lolspeak.po', $po_contents);
parent::setUpLanguage();
}
/**
* {@inheritdoc}
*/
protected function setUpProfile(): void {
// Do nothing, because this test only tests the language installation
// step's results.
}
/**
* {@inheritdoc}
*/
protected function setUpSettings(): void {
// Do nothing, because this test only tests the language installation
// step's results.
}
/**
* {@inheritdoc}
*/
protected function setUpRequirementsProblem(): void {
// Do nothing, because this test only tests the language installation
// step's results.
}
/**
* {@inheritdoc}
*/
protected function setUpSite(): void {
// Do nothing, because this test only tests the language installation
// step's results.
}
/**
* Ensures language selection has not failed.
*/
public function testInstall(): void {
// At this point we'll be on the profile selection or requirements screen.
$this->assertSession()->statusCodeEquals(200);
}
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
/**
* Tests translation files for multiple languages get imported during install.
*
* @group Installer
*/
class InstallerTranslationMultipleLanguageForeignTest extends InstallerTranslationMultipleLanguageTest {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Overrides the language code in which to install Drupal.
*
* @var string
*/
protected $langcode = 'de';
/**
* {@inheritdoc}
*/
protected function setUpLanguage(): void {
parent::setUpLanguage();
$this->translations['Save and continue'] = 'Save and continue de';
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
/**
* Tests that keeping English in a foreign language install works.
*
* @group Installer
*/
class InstallerTranslationMultipleLanguageKeepEnglishTest extends InstallerTranslationMultipleLanguageForeignTest {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Switch to the multilingual testing profile with English kept.
*
* @var string
*/
protected $profile = 'testing_multilingual_with_english';
}

View File

@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Tests\BrowserTestBase;
// cspell:ignore montag
/**
* Tests translation files for multiple languages get imported during install.
*
* @group Installer
*/
class InstallerTranslationMultipleLanguageNonInteractiveTest extends BrowserTestBase {
use StringTranslationTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Switch to the multilingual testing profile.
*
* @var string
*/
protected $profile = 'testing_multilingual';
/**
* {@inheritdoc}
*/
protected function prepareEnvironment(): void {
parent::prepareEnvironment();
// Place custom local translations in the translations directory.
mkdir(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE);
file_put_contents(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.de.po', $this->getPo('de'));
file_put_contents(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.es.po', $this->getPo('es'));
}
/**
* Returns the string for the test .po file.
*
* @param string $langcode
* The language code.
*
* @return string
* Contents for the test .po file.
*/
protected function getPo($langcode): string {
return <<<PO
msgid ""
msgstr ""
msgid "Save and continue"
msgstr "Save and continue $langcode"
msgid "Anonymous"
msgstr "Anonymous $langcode"
msgid "Language"
msgstr "Language $langcode"
#: Testing site name configuration during the installer.
msgid "Drupal"
msgstr "Drupal"
PO;
}
/**
* {@inheritdoc}
*/
protected function installParameters() {
$params = parent::installParameters();
$params['forms']['install_configure_form']['site_name'] = 'SITE_NAME_en';
return $params;
}
/**
* Tests that translations ended up at the expected places.
*/
public function testTranslationsLoaded(): void {
$this->drupalLogin($this->createUser([], NULL, TRUE));
// Ensure the title is correct.
$this->assertEquals('SITE_NAME_en', \Drupal::config('system.site')->get('name'));
// Verify German and Spanish were configured.
$this->drupalGet('admin/config/regional/language');
$this->assertSession()->pageTextContains('German');
$this->assertSession()->pageTextContains('Spanish');
// If the installer was English or we used a profile that keeps English, we
// expect that configured also. Otherwise English should not be configured
// on the site.
$this->assertSession()->pageTextContains('English');
// Verify the strings from the translation files were imported.
$this->verifyImportedStringsTranslated();
/** @var \Drupal\language\ConfigurableLanguageManager $language_manager */
$language_manager = \Drupal::languageManager();
// If the site was installed in a foreign language (only tested with German
// in subclasses), then the active configuration should be updated and no
// override should exist in German. Otherwise the German translation should
// end up in overrides the same way as Spanish (which is not used as a site
// installation language). English should be available based on profile
// information and should be possible to add if not yet added, making
// English overrides available.
$config = \Drupal::config('user.settings');
$override_de = $language_manager->getLanguageConfigOverride('de', 'user.settings');
$override_en = $language_manager->getLanguageConfigOverride('en', 'user.settings');
$override_es = $language_manager->getLanguageConfigOverride('es', 'user.settings');
// Active configuration should be English.
$this->assertEquals('Anonymous', $config->get('anonymous'));
$this->assertEquals('en', $config->get('langcode'));
// There should not be an English override.
$this->assertTrue($override_en->isNew());
// German should be an override.
$this->assertEquals('Anonymous de', $override_de->get('anonymous'));
// Spanish is always an override (never used as installation language).
$this->assertEquals('Anonymous es', $override_es->get('anonymous'));
// Test translation from locale_test module.
$this->assertEquals('Montag', $this->t('Monday', [], ['langcode' => 'de']));
}
/**
* Helper function to verify that the expected strings are translated.
*/
protected function verifyImportedStringsTranslated(): void {
$test_samples = ['Save and continue', 'Anonymous', 'Language'];
$langcodes = ['de', 'es'];
foreach ($test_samples as $sample) {
foreach ($langcodes as $langcode) {
$edit = [];
$edit['langcode'] = $langcode;
$edit['translation'] = 'translated';
$edit['string'] = $sample;
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($edit, 'Filter');
$this->assertSession()->pageTextContains($sample . ' ' . $langcode);
}
}
}
}

View File

@ -0,0 +1,187 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
/**
* Tests translation files for multiple languages get imported during install.
*
* @group Installer
*/
class InstallerTranslationMultipleLanguageTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Switch to the multilingual testing profile.
*
* @var string
*/
protected $profile = 'testing_multilingual';
/**
* {@inheritdoc}
*/
protected function setUpLanguage(): void {
// Place custom local translations in the translations directory.
mkdir(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE);
file_put_contents(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.de.po', $this->getPo('de'));
file_put_contents(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.es.po', $this->getPo('es'));
parent::setUpLanguage();
}
/**
* Returns the string for the test .po file.
*
* @param string $langcode
* The language code.
*
* @return string
* Contents for the test .po file.
*/
protected function getPo($langcode): string {
return <<<PO
msgid ""
msgstr ""
msgid "Save and continue"
msgstr "Save and continue $langcode"
msgid "Anonymous"
msgstr "Anonymous $langcode"
msgid "Language"
msgstr "Language $langcode"
#: Testing site name configuration during the installer.
msgid "Drupal"
msgstr "Drupal"
PO;
}
/**
* {@inheritdoc}
*/
protected function installParameters() {
$params = parent::installParameters();
$params['forms']['install_configure_form']['site_name'] = 'SITE_NAME_' . $this->langcode;
return $params;
}
/**
* Tests that translations ended up at the expected places.
*/
public function testTranslationsLoaded(): void {
// Ensure the title is correct.
$this->assertEquals('SITE_NAME_' . $this->langcode, \Drupal::config('system.site')->get('name'));
// Verify German and Spanish were configured.
$this->drupalGet('admin/config/regional/language');
$this->assertSession()->pageTextContains('German');
$this->assertSession()->pageTextContains('Spanish');
// If the installer was English or we used a profile that keeps English, we
// expect that configured also. Otherwise English should not be configured
// on the site.
if ($this->langcode == 'en' || $this->profile == 'testing_multilingual_with_english') {
$this->assertSession()->pageTextContains('English');
}
else {
$this->assertSession()->pageTextNotContains('English');
}
// Verify the strings from the translation files were imported.
$this->verifyImportedStringsTranslated();
/** @var \Drupal\language\ConfigurableLanguageManager $language_manager */
$language_manager = \Drupal::languageManager();
// If the site was installed in a foreign language (only tested with German
// in subclasses), then the active configuration should be updated and no
// override should exist in German. Otherwise the German translation should
// end up in overrides the same way as Spanish (which is not used as a site
// installation language). English should be available based on profile
// information and should be possible to add if not yet added, making
// English overrides available.
$config = \Drupal::config('user.settings');
$override_de = $language_manager->getLanguageConfigOverride('de', 'user.settings');
$override_en = $language_manager->getLanguageConfigOverride('en', 'user.settings');
$override_es = $language_manager->getLanguageConfigOverride('es', 'user.settings');
if ($this->langcode == 'de') {
// Active configuration should be in German and no German override should
// exist.
$this->assertEquals('Anonymous de', $config->get('anonymous'));
$this->assertEquals('de', $config->get('langcode'));
$this->assertTrue($override_de->isNew());
if ($this->profile == 'testing_multilingual_with_english') {
// English is already added in this profile. Should make the override
// available.
$this->assertEquals('Anonymous', $override_en->get('anonymous'));
}
else {
// English is not yet available.
$this->assertTrue($override_en->isNew());
// Adding English should make the English override available.
$edit = ['predefined_langcode' => 'en'];
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm($edit, 'Add language');
$override_en = $language_manager->getLanguageConfigOverride('en', 'user.settings');
$this->assertEquals('Anonymous', $override_en->get('anonymous'));
}
// Activate a module, to make sure that config is not overridden by module
// installation.
$edit = [
'modules[views][enable]' => TRUE,
'modules[filter][enable]' => TRUE,
];
$this->drupalGet('admin/modules');
$this->submitForm($edit, 'Install');
// Verify the strings from the translation are still as expected.
$this->verifyImportedStringsTranslated();
}
else {
// Active configuration should be English.
$this->assertEquals('Anonymous', $config->get('anonymous'));
$this->assertEquals('en', $config->get('langcode'));
// There should not be an English override.
$this->assertTrue($override_en->isNew());
// German should be an override.
$this->assertEquals('Anonymous de', $override_de->get('anonymous'));
}
// Spanish is always an override (never used as installation language).
$this->assertEquals('Anonymous es', $override_es->get('anonymous'));
}
/**
* Helper function to verify that the expected strings are translated.
*/
protected function verifyImportedStringsTranslated(): void {
$test_samples = ['Save and continue', 'Anonymous', 'Language'];
$langcodes = ['de', 'es'];
foreach ($test_samples as $sample) {
foreach ($langcodes as $langcode) {
$edit = [];
$edit['langcode'] = $langcode;
$edit['translation'] = 'translated';
$edit['string'] = $sample;
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($edit, 'Filter');
$this->assertSession()->pageTextContains($sample . ' ' . $langcode);
}
}
}
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Tests\BrowserTestBase;
/**
* Tests non-standard named translation files get imported during install.
*
* @group Installer
*/
class InstallerTranslationNonStandardFilenamesTest extends InstallerTranslationMultipleLanguageNonInteractiveTest {
/**
* {@inheritdoc}
*/
protected function prepareEnvironment(): void {
BrowserTestBase::prepareEnvironment();
// Place custom local translations in the translations directory.
mkdir(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE);
file_put_contents(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations/drupal.de.po', $this->getPo('de'));
file_put_contents(DRUPAL_ROOT . '/' . $this->siteDirectory . '/files/translations/drupal.es.po', $this->getPo('es'));
}
/**
* {@inheritdoc}
*/
protected function prepareSettings(): void {
parent::prepareSettings();
$settings['config']['locale.settings']['translation']['default_filename'] = (object) [
'value' => '%project.%language.po',
'required' => TRUE,
];
$settings['config']['locale.settings']['translation']['default_server_pattern'] = (object) [
'value' => 'translations://%project.%language.po',
'required' => TRUE,
];
$this->writeSettings($settings);
}
}

View File

@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
/**
* Installs Drupal in German and checks resulting site.
*
* @group Installer
*
* @see \Drupal\FunctionalTests\Installer\InstallerTranslationTest
*/
class InstallerTranslationQueryTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Overrides the language code in which to install Drupal.
*
* @var string
*/
protected $langcode = 'de';
/**
* {@inheritdoc}
*/
protected function visitInstaller(): void {
// Place a custom local translation in the translations directory.
mkdir($this->root . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE);
file_put_contents($this->root . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.de.po', $this->getPo('de'));
// The unrouted URL assembler does not exist at this point, so we build the
// URL ourselves.
$this->drupalGet($GLOBALS['base_url'] . '/core/install.php?langcode=' . $this->langcode);
// The language should have been automatically detected, all following
// screens should be translated already.
$this->assertSession()->buttonExists('Save and continue de');
$this->translations['Save and continue'] = 'Save and continue de';
// Check the language direction.
$this->assertSession()->elementTextEquals('xpath', '/@dir', 'ltr');
}
/**
* {@inheritdoc}
*/
protected function setUpLanguage(): void {
// The language was preset by passing a query parameter in the URL, so no
// explicit language selection is necessary.
}
/**
* Verifies the expected behaviors of the installation result.
*/
public function testInstaller(): void {
$this->assertSession()->addressEquals('user/1');
$this->assertSession()->statusCodeEquals(200);
// Verify German was configured but not English.
$this->drupalGet('admin/config/regional/language');
// cspell:ignore deutsch
$this->assertSession()->pageTextContains('Deutsch');
$this->assertSession()->pageTextNotContains('English');
}
/**
* Returns the string for the test .po file.
*
* @param string $langcode
* The language code.
*
* @return string
* Contents for the test .po file.
*/
protected function getPo($langcode): string {
return <<<PO
msgid ""
msgstr ""
msgid "Save and continue"
msgstr "Save and continue $langcode"
PO;
}
}

View File

@ -0,0 +1,194 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Database\Database;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\user\Entity\User;
/**
* Installs Drupal in German and checks resulting site.
*
* @group Installer
*/
class InstallerTranslationTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'test_theme';
/**
* Overrides the language code in which to install Drupal.
*
* @var string
*/
protected $langcode = 'de';
/**
* {@inheritdoc}
*/
protected function setUpLanguage(): void {
// Place a custom local translation in the translations directory.
mkdir($this->root . '/' . $this->siteDirectory . '/files/translations', 0777, TRUE);
file_put_contents($this->root . '/' . $this->siteDirectory . '/files/translations/drupal-8.0.0.de.po', $this->getPo('de'));
parent::setUpLanguage();
// After selecting a different language than English, all following screens
// should be translated already.
$this->assertSession()->buttonExists('Save and continue de');
$this->translations['Save and continue'] = 'Save and continue de';
// Check the language direction.
$this->assertSession()->elementTextEquals('xpath', '/@dir', 'ltr');
}
/**
* {@inheritdoc}
*/
protected function setUpSettings(): void {
// We are creating a table here to force an error in the installer because
// it will try and create the drupal_install_test table as this is part of
// the standard database tests performed by the installer in
// Drupal\Core\Database\Install\Tasks.
$spec = [
'fields' => [
'id' => [
'type' => 'int',
'not null' => TRUE,
],
],
'primary key' => ['id'],
];
Database::getConnection('default')->schema()->createTable('drupal_install_test', $spec);
parent::setUpSettings();
// Ensure that the error message translation is working.
// cSpell:disable
$this->assertSession()->responseContains('Beheben Sie alle Probleme unten, um die Installation fortzusetzen. Informationen zur Konfiguration der Datenbankserver finden Sie in der <a href="https://www.drupal.org/docs/installing-drupal">Installationshandbuch</a>, oder kontaktieren Sie Ihren Hosting-Anbieter.');
$this->assertSession()->responseContains('<strong>CREATE</strong> ein Test-Tabelle auf Ihrem Datenbankserver mit dem Befehl <em class="placeholder">CREATE TABLE {drupal_install_test} (id int NOT NULL PRIMARY KEY)</em> fehlgeschlagen.');
// cSpell:enable
// Now do it successfully.
Database::getConnection('default')->schema()->dropTable('drupal_install_test');
parent::setUpSettings();
}
/**
* Verifies the expected behaviors of the installation result.
*/
public function testInstaller(): void {
$this->assertSession()->addressEquals('user/1');
$this->assertSession()->statusCodeEquals(200);
// Verify German was configured but not English.
$this->drupalGet('admin/config/regional/language');
// cspell:ignore deutsch
$this->assertSession()->pageTextContains('Deutsch');
$this->assertSession()->pageTextNotContains('English');
// The current container still has the english as current language, rebuild.
$this->rebuildContainer();
/** @var \Drupal\user\Entity\User $account */
$account = User::load(0);
$this->assertEquals('de', $account->language()->getId(), 'Anonymous user is German.');
$account = User::load(1);
$this->assertEquals('de', $account->language()->getId(), 'Administrator user is German.');
$account = $this->drupalCreateUser();
$this->assertEquals('de', $account->language()->getId(), 'New user is German.');
// Ensure that we can enable basic_auth on a non-english site.
$this->drupalGet('admin/modules');
$this->submitForm(['modules[basic_auth][enable]' => TRUE], 'Install');
$this->assertSession()->statusCodeEquals(200);
// Assert that the theme CSS was added to the page.
$edit = ['preprocess_css' => FALSE];
$this->drupalGet('admin/config/development/performance');
$this->submitForm($edit, 'Save configuration');
$this->drupalGet('<front>');
$this->assertSession()->responseContains('my_theme/css/my-container-inline.css');
// Verify the strings from the translation files were imported.
$test_samples = ['Save and continue', 'Anonymous'];
foreach ($test_samples as $sample) {
$edit = [];
$edit['langcode'] = 'de';
$edit['translation'] = 'translated';
$edit['string'] = $sample;
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($edit, 'Filter');
$this->assertSession()->pageTextContains($sample . ' de');
}
/** @var \Drupal\language\ConfigurableLanguageManager $language_manager */
$language_manager = \Drupal::languageManager();
// Installed in German, configuration should be in German. No German or
// English overrides should be present.
$config = \Drupal::config('user.settings');
$override_de = $language_manager->getLanguageConfigOverride('de', 'user.settings');
$override_en = $language_manager->getLanguageConfigOverride('en', 'user.settings');
$this->assertEquals('Anonymous de', $config->get('anonymous'));
$this->assertEquals('de', $config->get('langcode'));
$this->assertTrue($override_de->isNew());
$this->assertTrue($override_en->isNew());
// Assert that adding English makes the English override available.
$edit = ['predefined_langcode' => 'en'];
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm($edit, 'Add language');
$override_en = $language_manager->getLanguageConfigOverride('en', 'user.settings');
$this->assertFalse($override_en->isNew());
$this->assertSession()->pageTextContains('English de');
$this->assertEquals('Anonymous', $override_en->get('anonymous'));
$english = ConfigurableLanguage::load('en');
$this->assertEquals('de', $english->language()->getId(), 'The langcode of the english language is de.');
// English is guaranteed to be the second language, click the second
// language edit link.
$this->clickLink('Edit', 1);
$this->assertSession()->fieldValueEquals('label', 'English de');
$this->submitForm([], 'Save language');
$english = ConfigurableLanguage::load('en');
$this->assertEquals('de', $english->language()->getId(), 'The langcode of the english language is de.');
}
/**
* Returns the string for the test .po file.
*
* @param string $langcode
* The language code.
*
* @return string
* Contents for the test .po file.
*/
protected function getPo($langcode): string {
return <<<PO
msgid ""
msgstr ""
msgid "Save and continue"
msgstr "Save and continue $langcode"
msgid "Anonymous"
msgstr "Anonymous $langcode"
msgid "English"
msgstr "English $langcode"
msgid "Resolve all issues below to continue the installation. For help configuring your database server, see the <a href="https://www.drupal.org/docs/installing-drupal">installation handbook</a>, or contact your hosting provider."
msgstr "Beheben Sie alle Probleme unten, um die Installation fortzusetzen. Informationen zur Konfiguration der Datenbankserver finden Sie in der <a href="https://www.drupal.org/docs/installing-drupal">Installationshandbuch</a>, oder kontaktieren Sie Ihren Hosting-Anbieter."
msgid "Failed to <strong>CREATE</strong> a test table on your database server with the command %query. The server reports the following message: %error.<p>Are you sure the configured username has the necessary permissions to create tables in the database?</p>"
msgstr "<strong>CREATE</strong> ein Test-Tabelle auf Ihrem Datenbankserver mit dem Befehl %query fehlgeschlagen."
PO;
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\KernelTests\AssertConfigTrait;
/**
* Tests the interactive installer installing the minimal profile.
*
* @group Installer
*/
class MinimalInstallerTest extends ConfigAfterInstallerTestBase {
use AssertConfigTrait;
/**
* {@inheritdoc}
*/
protected $profile = 'minimal';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Ensures that the exported minimal configuration is up to date.
*/
public function testMinimalConfig(): void {
$this->assertInstalledConfig([]);
}
}

View File

@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Component\Serialization\Yaml;
/**
* Tests multiple distribution profile support.
*
* @group Installer
*/
class MultipleDistributionsProfileTest extends InstallerTestBase {
/**
* The distribution profile info.
*
* @var array
*/
protected $info;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function prepareEnvironment(): void {
parent::prepareEnvironment();
// Create two distributions.
foreach (['distribution_one', 'distribution_two'] as $name) {
$info = [
'type' => 'profile',
'core_version_requirement' => '*',
'name' => $name . ' profile',
'distribution' => [
'name' => $name,
'install' => [
'theme' => 'claro',
],
],
];
// File API functions are not available yet.
$path = $this->root . DIRECTORY_SEPARATOR . $this->siteDirectory . '/profiles/' . $name;
mkdir($path, 0777, TRUE);
file_put_contents("$path/$name.info.yml", Yaml::encode($info));
}
// Install the first distribution.
$this->profile = 'distribution_one';
}
/**
* {@inheritdoc}
*/
protected function setUpLanguage(): void {
// Verify that the distribution name appears.
$this->assertSession()->pageTextContains('distribution_one');
// Verify that the requested theme is used.
$this->assertSession()->responseContains('claro');
// Verify that the "Choose profile" step does not appear.
$this->assertSession()->pageTextNotContains('profile');
parent::setUpLanguage();
}
/**
* {@inheritdoc}
*/
protected function setUpProfile(): void {
// This step is skipped, because there is a distribution profile.
}
/**
* Confirms that the installation succeeded.
*/
public function testInstalled(): void {
$this->assertSession()->addressEquals('user/1');
$this->assertSession()->statusCodeEquals(200);
// Confirm that we are logged-in after installation.
$this->assertSession()->pageTextContains($this->rootUser->getAccountName());
// Confirm that Drupal recognizes this distribution as the current profile.
$this->assertEquals('distribution_one', \Drupal::installProfile());
$this->assertEquals('distribution_one', $this->config('core.extension')->get('profile'), 'The install profile has been written to core.extension configuration.');
$this->rebuildContainer();
$this->assertEquals('distribution_one', \Drupal::installProfile());
}
}

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
/**
* Tests installing a profile that implements InstallRequirementsInterface.
*
* @group Installer
*/
class ProfileRequirementsTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $profile = 'profile_install_requirements';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUpSettings(): void {
// This form will never be reached.
}
/**
* {@inheritdoc}
*/
protected function setUpRequirementsProblem(): void {
// The parent method asserts that there are no requirements errors, but
// this test expects a requirements error in the test method below.
// Therefore, we override this method to suppress the parent's assertions.
}
/**
* {@inheritdoc}
*/
protected function setUpSite(): void {
// This form will never be reached.
}
/**
* Test Requirements are picked up.
*/
public function testRequirementsFailure(): void {
$this->assertSession()->pageTextContains('Testing requirements failed requirements.');
}
}

View File

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Serialization\Yaml;
/**
* Tests distribution profile support.
*
* @group Installer
*/
class SingleVisibleProfileTest extends InstallerTestBase {
/**
* The installation profile to install.
*
* Not needed when only one is visible.
*
* @var string
*/
protected $profile = NULL;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function prepareEnvironment(): void {
parent::prepareEnvironment();
$profiles = ['standard', 'demo_umami'];
foreach ($profiles as $profile) {
$info = [
'type' => 'profile',
'core_version_requirement' => '^8 || ^9 || ^10',
'name' => 'Override ' . $profile,
'hidden' => TRUE,
];
// File API functions are not available yet.
$path = $this->siteDirectory . '/profiles/' . $profile;
mkdir($path, 0777, TRUE);
file_put_contents("$path/$profile.info.yml", Yaml::encode($info));
}
}
/**
* {@inheritdoc}
*/
protected function setUpProfile(): void {
// This step is skipped, because there is only one visible profile.
}
/**
* Confirms that the installation succeeded.
*/
public function testInstalled(): void {
$this->assertSession()->addressEquals('user/1');
$this->assertSession()->statusCodeEquals(200);
// Confirm that we are logged-in after installation.
$this->assertSession()->pageTextContains($this->rootUser->getAccountName());
// Confirm that the minimal profile was installed.
$this->assertEquals('minimal', \Drupal::installProfile());
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Tests\BrowserTestBase;
/**
* Tests that the site name can be set during a non-interactive installation.
*
* @group Installer
*/
class SiteNameTest extends BrowserTestBase {
/**
* The site name to be used when testing.
*
* @var string
*/
protected $siteName;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function installParameters() {
$this->siteName = $this->randomMachineName();
$parameters = parent::installParameters();
$parameters['forms']['install_configure_form']['site_name'] = $this->siteName;
return $parameters;
}
/**
* Tests that the desired site name appears on the page after installation.
*/
public function testSiteName(): void {
$this->drupalGet('');
$this->assertSession()->pageTextContains($this->siteName);
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the extension of the site settings form.
*
* @group Installer
*/
class SiteSettingsFormTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['install_form_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Confirms that the form is extensible.
*/
public function testSiteSettingsForm(): void {
// Test that the form page can be loaded without errors.
$this->drupalGet('test-form');
$this->assertSession()->statusCodeEquals(200);
}
}

View File

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
/**
* Tests the interactive installer installing the standard profile.
*
* @group Installer
*/
class StandardInstallerTest extends ConfigAfterInstallerTestBase {
/**
* {@inheritdoc}
*/
protected $profile = 'standard';
/**
* Ensures that the user page is available after installation.
*/
public function testInstaller(): void {
// Verify that Olivero's default frontpage appears.
$this->assertSession()->pageTextContains('Congratulations and welcome to the Drupal community.');
$this->assertSession()->elementTextContains('css', '#block-olivero-powered', 'Powered by Drupal');
}
/**
* {@inheritdoc}
*/
protected function setUpSite(): void {
// Test that the correct theme is being used.
$this->assertSession()->responseNotContains('olivero');
$this->assertSession()->responseContains('css/theme/install-page.css');
parent::setUpSite();
}
/**
* Ensures that the exported standard configuration is up to date.
*/
public function testStandardConfig(): void {
$skipped_config = [];
// FunctionalTestSetupTrait::installParameters() uses Drupal as site name
// and simpletest@example.com as mail address.
$skipped_config['system.site'][] = 'name: Drupal';
$skipped_config['system.site'][] = 'mail: simpletest@example.com';
$skipped_config['contact.form.feedback'][] = '- simpletest@example.com';
// \Drupal\filter\Entity\FilterFormat::toArray() drops the roles of filter
// formats.
$skipped_config['filter.format.basic_html'][] = 'roles:';
$skipped_config['filter.format.basic_html'][] = '- authenticated';
$skipped_config['filter.format.full_html'][] = 'roles:';
$skipped_config['filter.format.full_html'][] = '- administrator';
$skipped_config['filter.format.restricted_html'][] = 'roles:';
$skipped_config['filter.format.restricted_html'][] = '- anonymous';
// The site UUID is set dynamically for each installation.
$skipped_config['system.site'][] = 'uuid: ' . $this->config('system.site')->get('uuid');
$this->assertInstalledConfig($skipped_config);
}
}

View File

@ -0,0 +1,150 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Serialization\Yaml;
use Drupal\user\Entity\User;
/**
* Tests superuser access and the installer.
*
* @group Installer
*/
class SuperUserAccessInstallTest extends InstallerTestBase {
/**
* Message when the logged-in user does not have admin access after install.
*
* @see \Drupal\Core\Installer\Form\SiteConfigureForm::submitForm())
*/
protected const NO_ACCESS_MESSAGE = 'The user %s does not have administrator access. For more information, see the documentation on securing the admin super user.';
/**
* {@inheritdoc}
*/
protected $profile = 'superuser';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function prepareEnvironment(): void {
parent::prepareEnvironment();
$info = [
'type' => 'profile',
'core_version_requirement' => '*',
'name' => 'Superuser testing profile',
];
// File API functions are not available yet.
$path = $this->siteDirectory . '/profiles/superuser';
mkdir($path, 0777, TRUE);
file_put_contents("$path/superuser.info.yml", Yaml::encode($info));
file_put_contents("$path/superuser.install", $this->providedData()['install_code']);
$services = Yaml::decode(file_get_contents(DRUPAL_ROOT . '/sites/default/default.services.yml'));
$services['parameters']['security.enable_super_user'] = $this->providedData()['super_user_policy'];
file_put_contents(DRUPAL_ROOT . '/' . $this->siteDirectory . '/services.yml', Yaml::encode($services));
}
/**
* {@inheritdoc}
*/
protected function setUpSite(): void {
if ($this->providedData()['super_user_policy'] === FALSE && empty($this->providedData()['expected_roles'])) {
$this->assertSession()->pageTextContains('Site account');
$this->assertSession()->pageTextNotContains('Site maintenance account');
}
else {
$this->assertSession()->pageTextNotContains('Site account');
$this->assertSession()->pageTextContains('Site maintenance account');
}
parent::setUpSite();
}
/**
* Confirms that the installation succeeded.
*
* @dataProvider getInstallTests
*/
public function testInstalled(bool $expected_runtime_has_permission, bool $expected_no_access_message, array $expected_roles, string $install_code, bool $super_user_policy): void {
$user = User::load(1);
$this->assertSame($expected_runtime_has_permission, $user->hasPermission('administer software updates'));
$this->assertTrue(\Drupal::state()->get('admin_permission_in_installer'));
$message = sprintf(static::NO_ACCESS_MESSAGE, $this->rootUser->getDisplayName());
if ($expected_no_access_message) {
$this->assertSession()->pageTextContains($message);
}
else {
$this->assertSession()->pageTextNotContains($message);
}
$this->assertSame($expected_roles, $user->getRoles(TRUE));
}
public static function getInstallTests(): array {
$test_cases = [];
$test_cases['runtime super user policy enabled'] = [
'expected_runtime_has_permission' => TRUE,
'expected_no_access_message' => FALSE,
'expected_roles' => [],
'install_code' => <<<PHP
<?php
function superuser_install() {
\$user = \Drupal\user\Entity\User::load(1);
\Drupal::state()->set('admin_permission_in_installer', \$user->hasPermission('administer software updates'));
}
PHP,
'super_user_policy' => TRUE,
];
$test_cases['no super user policy enabled and no admin role'] = [
'expected_runtime_has_permission' => FALSE,
'expected_no_access_message' => TRUE,
'expected_roles' => [],
'install_code' => $test_cases['runtime super user policy enabled']['install_code'],
'super_user_policy' => FALSE,
];
$test_cases['no super user policy enabled and admin role'] = [
'expected_runtime_has_permission' => TRUE,
'expected_no_access_message' => FALSE,
'expected_roles' => ['admin_role'],
'install_code' => <<<PHP
<?php
function superuser_install() {
\$user = \Drupal\user\Entity\User::load(1);
\Drupal::state()->set('admin_permission_in_installer', \$user->hasPermission('administer software updates'));
\Drupal\user\Entity\Role::create(['id' => 'admin_role', 'label' => 'Admin role'])->setIsAdmin(TRUE)->save();
\Drupal\user\Entity\Role::create(['id' => 'another_role', 'label' => 'Another role'])->save();
}
PHP,
'super_user_policy' => FALSE,
];
$test_cases['no super user policy enabled and multiple admin role'] = [
'expected_runtime_has_permission' => TRUE,
'expected_no_access_message' => FALSE,
'expected_roles' => ['admin_role', 'another_admin_role'],
'install_code' => <<<PHP
<?php
function superuser_install() {
\$user = \Drupal\user\Entity\User::load(1);
\Drupal::state()->set('admin_permission_in_installer', \$user->hasPermission('administer software updates'));
\Drupal\user\Entity\Role::create(['id' => 'admin_role', 'label' => 'Admin role'])->setIsAdmin(TRUE)->save();
\Drupal\user\Entity\Role::create(['id' => 'another_admin_role', 'label' => 'Another admin role'])->setIsAdmin(TRUE)->save();
\Drupal\user\Entity\Role::create(['id' => 'another_role', 'label' => 'Another role'])->save();
}
PHP,
'super_user_policy' => FALSE,
];
return $test_cases;
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Tests\BrowserTestBase;
/**
* Tests installing the Testing profile with update notifications on.
*
* @group Installer
*/
class TestingProfileHooksTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected $profile = 'testing_hooks';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Test hooks are picked up.
*/
public function testHookPickup(): void {
$this->assertFalse(isset($GLOBALS['profile_procedural']));
$this->assertFalse(isset($GLOBALS['profile_oop']));
drupal_flush_all_caches();
$this->assertTrue(isset($GLOBALS['profile_procedural']));
$this->assertTrue(isset($GLOBALS['profile_oop']));
}
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Drupal\FunctionalTests\Installer;
use Drupal\Tests\BrowserTestBase;
/**
* Tests installing the Testing profile with update notifications on.
*
* @group Installer
*/
class TestingProfileInstallTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected $profile = 'testing';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Ensure the Update Status module is installed.
*/
public function testUpdateModuleInstall(): void {
$this->assertTrue(\Drupal::moduleHandler()->moduleExists('update'));
}
/**
* {@inheritdoc}
*/
protected function installParameters() {
$params = parent::installParameters();
$params['forms']['install_configure_form']['enable_update_status_module'] = TRUE;
return $params;
}
}