class ConfigImportCommandTest
Tests the 'config:import' command.
This command imports staged configuration the same way a user can from the Configuration module's synchronize page, except run from the command line.
This test is a functional test because we use the config importer.
Attributes
#[Group('config')]
#[RunTestsInSeparateProcesses]
#[CoversClass(ConfigImportCommand::class)]
Hierarchy
- class \Drupal\Tests\DrupalTestCase uses \Drupal\Tests\DrupalTestCaseTrait extends \PHPUnit\Framework\TestCase
- class \Drupal\Tests\BrowserTestBase uses \Drupal\Core\Test\FunctionalTestSetupTrait, \Drupal\Tests\UiHelperTrait, \Drupal\Core\Test\TestSetupTrait, \Drupal\Tests\block\Traits\BlockCreationTrait, \Drupal\Tests\RandomGeneratorTrait, \Drupal\Tests\node\Traits\NodeCreationTrait, \Drupal\Tests\node\Traits\ContentTypeCreationTrait, \Drupal\Tests\ConfigTestTrait, \Drupal\Tests\user\Traits\UserCreationTrait, \Drupal\Tests\XdebugRequestTrait, \Drupal\Tests\ExtensionListTestTrait extends \Drupal\Tests\DrupalTestCase
- class \Drupal\FunctionalTests\Core\Config\ConfigImportCommandTest extends \Drupal\Tests\BrowserTestBase
- class \Drupal\Tests\BrowserTestBase uses \Drupal\Core\Test\FunctionalTestSetupTrait, \Drupal\Tests\UiHelperTrait, \Drupal\Core\Test\TestSetupTrait, \Drupal\Tests\block\Traits\BlockCreationTrait, \Drupal\Tests\RandomGeneratorTrait, \Drupal\Tests\node\Traits\NodeCreationTrait, \Drupal\Tests\node\Traits\ContentTypeCreationTrait, \Drupal\Tests\ConfigTestTrait, \Drupal\Tests\user\Traits\UserCreationTrait, \Drupal\Tests\XdebugRequestTrait, \Drupal\Tests\ExtensionListTestTrait extends \Drupal\Tests\DrupalTestCase
Expanded class hierarchy of ConfigImportCommandTest
See also
\Drupal\Tests\config\Functional\ConfigImportUITest
File
-
core/
tests/ Drupal/ FunctionalTests/ Core/ Config/ ConfigImportCommandTest.php, line 33
Namespace
Drupal\FunctionalTests\Core\ConfigView source
class ConfigImportCommandTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'config',
'config_test',
'config_import_test',
'text',
'options',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp() : void {
parent::setUp();
$this->copyConfig($this->container
->get('config.storage'), $this->container
->get('config.storage.sync'));
}
/**
* Tests importing configuration.
*
* @see \Drupal\Tests\config\Functional\ConfigImportUITest::testImport()
*/
public function testImport() : void {
$name = 'system.site';
$dynamic_name = 'config_test.dynamic.new';
$sync = $this->container
->get('config.storage.sync');
$tester = $this->commandTester();
$code = $tester->execute([]);
self::assertSame(Command::SUCCESS, $code);
self::assertStringContainsString('There are no changes to import.', $tester->getDisplay());
// Create updated configuration object.
$new_site_name = 'Config import test ' . $this->randomMachineName();
$this->prepareSiteNameUpdate($new_site_name);
self::assertTrue($sync->exists($name), $name . ' found.');
// Create new config entity.
$original_dynamic_data = [
'uuid' => '30df59bd-7b03-4cf7-bb35-d42fc49f0651',
'langcode' => \Drupal::languageManager()->getDefaultLanguage()
->getId(),
'status' => TRUE,
'dependencies' => [],
'id' => 'new',
'label' => 'New',
'weight' => 0,
'style' => '',
'size' => '',
'size_value' => '',
'protected_property' => '',
];
$sync->write($dynamic_name, $original_dynamic_data);
self::assertTrue($sync->exists($dynamic_name), $dynamic_name . ' found.');
// Enable the Automated Cron and config_install_schema_test modules during
// import. The config_install_schema_test module is used because it creates
// a table during the install. The Automated Cron module is used because it
// creates a single simple configuration file during the install.
$core_extension = $this->config('core.extension')
->get();
$core_extension['module']['automated_cron'] = 0;
$core_extension['module']['config_install_schema_test'] = 0;
$core_extension['module'] = \Drupal::service(ModuleWeight::class)->sort($core_extension['module']);
$core_extension['theme']['test_theme'] = 0;
$sync->write('core.extension', $core_extension);
// Use the install storage so that we can read configuration from modules
// and themes that are not installed.
$install_storage = new InstallStorage();
// Set the test theme as default.
$system_theme = $this->config('system.theme')
->get();
$system_theme['default'] = 'test_theme';
$sync->write('system.theme', $system_theme);
// Read the automated_cron config from module default config folder.
$settings = $install_storage->read('automated_cron.settings');
$settings['interval'] = 10000;
$sync->write('automated_cron.settings', $settings);
// Uninstall the Options and Text modules to ensure that dependencies are
// handled correctly. Options depends on Text so Text should be installed
// first. Since they were enabled during the test setup the core.extension
// file in sync will already contain them.
\Drupal::service('module_installer')->uninstall([
'text',
'options',
]);
// Set the state system to record installations and uninstallations.
\Drupal::state()->set('ConfigImportUITest.core.extension.modules_installed', []);
\Drupal::state()->set('ConfigImportUITest.core.extension.modules_uninstalled', []);
// Run the import command.
$tester = $this->commandTester();
$code = $tester->execute([
'--yes' => TRUE,
]);
self::assertSame(Command::SUCCESS, $code);
$display = preg_replace('/\\s+/', ' ', $tester->getDisplay());
// Verify that both appear as ready to import.
self::assertStringContainsString($name . ' Update', $display);
self::assertStringContainsString($dynamic_name . ' Create', $display);
self::assertStringContainsString('core.extension Update', $display);
self::assertStringContainsString('system.theme Update', $display);
self::assertStringContainsString('automated_cron.settings Create', $display);
self::assertStringContainsString('The configuration was imported successfully.', $display);
// Verify that there are no further changes to import.
$tester = $this->commandTester();
$code = $tester->execute([]);
$this->assertSame(Command::SUCCESS, $code);
$this->assertStringContainsString('There are no changes to import.', $tester->getDisplay());
$this->rebuildContainer();
// Verify site name has changed.
self::assertSame($new_site_name, $this->config($name)
->get('name'));
// Verify that new config entity exists.
self::assertSame($original_dynamic_data, $this->config($dynamic_name)
->get());
self::assertTrue(\Drupal::moduleHandler()->moduleExists('config_install_schema_test'), 'config_install_schema_test module installed during import.');
self::assertTrue(\Drupal::database()->schema()
->tableExists('config_install_schema_test_ip'), 'The database table config_install_schema_test_ip exists.');
self::assertTrue(\Drupal::moduleHandler()->moduleExists('automated_cron'), 'Automated Cron module installed during import.');
self::assertTrue(\Drupal::moduleHandler()->moduleExists('options'), 'Options module installed during import.');
self::assertTrue(\Drupal::moduleHandler()->moduleExists('text'), 'Text module installed during import.');
self::assertTrue(\Drupal::service('theme_handler')->themeExists('test_theme'), 'Test theme installed during import.');
// Ensure installations and uninstallation occur as expected.
$uninstalled = \Drupal::state()->get('ConfigImportUITest.core.extension.modules_uninstalled', []);
$expected = [
'automated_cron',
'config_install_schema_test',
'text',
'options',
];
$installed = \Drupal::state()->get('config_import_test_modules_installed.list');
self::assertSame($expected, $installed, 'Automated Cron, config_install_schema_test, Text and Options modules installed in the correct order.');
self::assertEmpty($uninstalled, 'No modules uninstalled during import');
// Verify that the automated_cron configuration object was only written
// once during the import process and only with the value set in the staged
// configuration. This verifies that the module's default configuration is
// used during configuration import and, additionally, that after installing
// a module, that configuration is not synced twice.
$interval_values = \Drupal::state()->get('ConfigImportUITest.automated_cron.settings.interval', []);
self::assertSame([
10000,
], $interval_values);
$core_extension = $this->config('core.extension')
->get();
unset($core_extension['module']['automated_cron']);
unset($core_extension['module']['config_install_schema_test']);
unset($core_extension['module']['options']);
unset($core_extension['module']['text']);
unset($core_extension['theme']['test_theme']);
$sync->write('core.extension', $core_extension);
$sync->delete('automated_cron.settings');
$sync->delete('text.settings');
$system_theme = $this->config('system.theme')
->get();
$system_theme = [
'_core' => $system_theme['_core'],
'admin' => 'stark',
'default' => 'stark',
];
$sync->write('system.theme', $system_theme);
// Set the state system to record installations and uninstallations.
\Drupal::state()->set('ConfigImportUITest.core.extension.modules_installed', []);
\Drupal::state()->set('ConfigImportUITest.core.extension.modules_uninstalled', []);
// Run the import command but cancel it.
$tester = $this->commandTester();
$tester->setInputs([
'no',
]);
try {
$tester->execute([], [
'interactive' => TRUE,
]);
$this->fail('Expected a UserAbortException to be thrown.');
} catch (UserAbortException) {
// Expected.
}
$display = preg_replace('/\\s+/', ' ', $tester->getDisplay());
self::assertStringContainsString('core.extension Update', $display);
self::assertStringContainsString('system.theme Update', $display);
self::assertStringContainsString('automated_cron.settings Delete', $display);
self::assertStringContainsString('text.settings Delete', $display);
self::assertStringContainsString('Import the listed configuration changes?', $display);
self::assertStringNotContainsString('The configuration was imported successfully.', $display);
self::assertEmpty(\Drupal::state()->get('ConfigImportUITest.core.extension.modules_installed', []));
self::assertEmpty(\Drupal::state()->get('ConfigImportUITest.core.extension.modules_uninstalled', []));
self::assertTrue(\Drupal::moduleHandler()->moduleExists('automated_cron'), 'Automated Cron module still installed.');
// Run the import command and confirm it.
$tester = $this->commandTester();
$tester->setInputs([
'yes',
]);
$code = $tester->execute([], [
'interactive' => TRUE,
]);
self::assertSame(Command::SUCCESS, $code);
$display = preg_replace('/\\s+/', ' ', $tester->getDisplay());
self::assertStringContainsString('core.extension Update', $display);
self::assertStringContainsString('system.theme Update', $display);
self::assertStringContainsString('automated_cron.settings Delete', $display);
self::assertStringContainsString('text.settings Delete', $display);
self::assertStringContainsString('Import the listed configuration changes?', $display);
self::assertStringContainsString('The configuration was imported successfully.', $display);
$this->rebuildContainer();
self::assertFalse(\Drupal::moduleHandler()->moduleExists('config_install_schema_test'), 'config_install_schema_test module uninstalled during import.');
self::assertFalse(\Drupal::database()->schema()
->tableExists('config_install_schema_test_ip'), 'The database table config_install_schema_test_ip does not exist.');
self::assertFalse(\Drupal::moduleHandler()->moduleExists('automated_cron'), 'Automated cron module uninstalled during import.');
self::assertFalse(\Drupal::moduleHandler()->moduleExists('options'), 'Options module uninstalled during import.');
self::assertFalse(\Drupal::moduleHandler()->moduleExists('text'), 'Text module uninstalled during import.');
// Ensure installations and uninstallation occur as expected.
$installed = \Drupal::state()->get('ConfigImportUITest.core.extension.modules_installed', []);
$uninstalled = \Drupal::state()->get('ConfigImportUITest.core.extension.modules_uninstalled', []);
$expected = [
'options',
'text',
'config_install_schema_test',
'automated_cron',
];
self::assertSame($expected, $uninstalled, 'Options, Text, config_install_schema_test and Automated Cron modules uninstalled in the correct order.');
self::assertEmpty($installed, 'No modules installed during import');
$theme_info = \Drupal::service('theme_handler')->listInfo();
self::assertFalse(isset($theme_info['test_theme']), 'Test theme uninstalled during import.');
// Verify that the automated_cron.settings configuration object was only
// deleted once during the import process.
$delete_called = \Drupal::state()->get('ConfigImportUITest.automated_cron.settings.delete', 0);
self::assertSame(1, $delete_called, "The automated_cron.settings configuration was deleted once during configuration import.");
}
/**
* Tests concurrent importing of configuration.
*
* @see \Drupal\Tests\config\Functional\ConfigImportUITest::testImportLock()
*/
public function testImportLock() : void {
$new_site_name = 'Config import test ' . $this->randomMachineName();
$this->prepareSiteNameUpdate($new_site_name);
// Acquire a fake lock on the import mechanism.
$lock = $this->container
->get('lock.persistent');
$lock->acquire(ConfigImporter::LOCK_NAME);
// Attempt to import configuration and verify that an error message appears.
$tester = $this->commandTester();
$code = $tester->execute([
'--yes' => TRUE,
]);
self::assertSame(Command::FAILURE, $code);
self::assertStringContainsString('Another request may be synchronizing configuration already.', $tester->getDisplay());
// Release the lock, just to keep testing sane.
$lock->release(ConfigImporter::LOCK_NAME);
// Verify site name has not changed.
self::assertNotEquals($new_site_name, $this->config('system.site')
->get('name'));
}
/**
* Tests verification of site UUID before importing configuration.
*
* @see \Drupal\Tests\config\Functional\ConfigImportUITest::testImportSiteUuidValidation()
*/
public function testImportSiteUuidValidation() : void {
$sync = $this->container
->get('config.storage.sync');
$config_data = $this->config('system.site')
->get();
$config_data['uuid'] = $this->container
->get('uuid')
->generate();
$sync->write('system.site', $config_data);
$tester = $this->commandTester();
$code = $tester->execute([
'--yes' => TRUE,
]);
self::assertSame(Command::FAILURE, $code);
self::assertStringContainsString('Site UUID in source storage does not match the target storage', $tester->getDisplay());
}
/**
* Tests an import that results in an error.
*/
public function testImportErrorLog() : void {
$name_primary = 'config_test.dynamic.primary';
$name_secondary = 'config_test.dynamic.secondary';
$sync = $this->container
->get('config.storage.sync');
$uuid = $this->container
->get('uuid');
$values_primary = [
'uuid' => $uuid->generate(),
'langcode' => 'en',
'status' => TRUE,
'dependencies' => [],
'id' => 'primary',
'label' => 'Primary',
'weight' => 0,
'style' => NULL,
'size' => NULL,
'size_value' => NULL,
'protected_property' => NULL,
];
$sync->write($name_primary, $values_primary);
$values_secondary = [
'uuid' => $uuid->generate(),
'langcode' => 'en',
'status' => TRUE,
// Add a dependency on primary, to ensure that is synced first.
'dependencies' => [
'config' => [
$name_primary,
],
],
'id' => 'secondary',
'label' => 'Secondary Sync',
'weight' => 0,
'style' => NULL,
'size' => NULL,
'size_value' => NULL,
'protected_property' => NULL,
];
$sync->write($name_secondary, $values_secondary);
// Run the import command.
$tester = $this->commandTester();
$code = $tester->execute([
'--yes' => TRUE,
]);
self::assertSame(Command::FAILURE, $code);
$display = preg_replace('/\\s+/', ' ', $tester->getDisplay());
// Verify that both appear as ready to import.
self::assertStringContainsString($name_primary . ' Create', $display);
self::assertStringContainsString($name_secondary . ' Create', $display);
self::assertStringContainsString('[ERROR] Deleted and replaced configuration entity "' . $name_secondary . '"', $display);
self::assertStringContainsString('[ERROR] The configuration was imported with errors.', $display);
self::assertStringNotContainsString('The configuration was imported successfully.', $display);
}
/**
* Tests that errors set in the batch and on the ConfigImporter are merged.
*/
public function testBatchErrors() : void {
$new_site_name = 'Config import test ' . $this->randomString();
$this->prepareSiteNameUpdate($new_site_name);
\Drupal::state()->set('config_import_steps_alter.error', TRUE);
$tester = $this->commandTester();
$code = $tester->execute([
'--yes' => TRUE,
]);
self::assertSame(Command::FAILURE, $code);
$display = preg_replace('/\\s+/', ' ', $tester->getDisplay());
// The dr command does not do batches yet.
self::assertStringNotContainsString('[ERROR] _config_import_test_config_import_steps_alter batch error', $display);
self::assertStringContainsString('[ERROR] _config_import_test_config_import_steps_alter ConfigImporter error', $display);
self::assertStringNotContainsString('The configuration was imported successfully.', $display);
self::assertStringContainsString('The configuration was imported with errors.', $display);
}
/**
* Tests that import storage transformation event subscribers run.
*
* @see \Drupal\Tests\config\Functional\TransformedConfigExportImportUITest
*/
public function testImportTransformation() : void {
\Drupal::service('module_installer')->install([
'config_transformer_test',
]);
$new_site_name = 'Config import test ' . $this->randomMachineName();
$this->prepareSiteNameUpdate($new_site_name);
$active_name = $this->config('system.site')
->get('name');
$tester = $this->commandTester();
$code = $tester->execute([
'--yes' => TRUE,
]);
self::assertSame(Command::SUCCESS, $code);
// \Drupal\config_transformer_test\EventSubscriber::onImportTransform()
// ignores the staged site name and appends " Arrr" to whatever name is
// currently active, so the transformed value is imported instead.
self::assertSame($active_name . ' Arrr', $this->config('system.site')
->get('name'));
}
/**
* Prepares a site name update by modifying the synchronized configuration.
*
* @see \Drupal\Tests\config\Functional\ConfigImportUITest::prepareSiteNameUpdate()
*/
private function prepareSiteNameUpdate(string $new_site_name) : void {
$sync = $this->container
->get('config.storage.sync');
$config_data = $this->config('system.site')
->get();
$config_data['name'] = $new_site_name;
$sync->write('system.site', $config_data);
}
/**
* Builds a command tester for the 'config:import' command.
*/
private function commandTester() : CommandTester {
$command = new ConfigImportCommand($this->container
->get('config.storage'), $this->container
->get('config.storage.sync'), $this->container
->get(ImportStorageTransformer::class), $this->container
->get(ConfigImporterFactory::class));
return new CommandTester($command);
}
}
Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.