ConfigImporterTest.php 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811
  1. <?php
  2. namespace Drupal\KernelTests\Core\Config;
  3. use Drupal\Component\Utility\Html;
  4. use Drupal\Component\Utility\SafeMarkup;
  5. use Drupal\Core\Config\ConfigImporter;
  6. use Drupal\Core\Config\ConfigImporterException;
  7. use Drupal\Core\Config\StorageComparer;
  8. use Drupal\KernelTests\KernelTestBase;
  9. /**
  10. * Tests importing configuration from files into active configuration.
  11. *
  12. * @group config
  13. */
  14. class ConfigImporterTest extends KernelTestBase {
  15. /**
  16. * Config Importer object used for testing.
  17. *
  18. * @var \Drupal\Core\Config\ConfigImporter
  19. */
  20. protected $configImporter;
  21. /**
  22. * Modules to enable.
  23. *
  24. * @var array
  25. */
  26. public static $modules = ['config_test', 'system', 'config_import_test'];
  27. protected function setUp() {
  28. parent::setUp();
  29. $this->installConfig(['config_test']);
  30. // Installing config_test's default configuration pollutes the global
  31. // variable being used for recording hook invocations by this test already,
  32. // so it has to be cleared out manually.
  33. unset($GLOBALS['hook_config_test']);
  34. $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync'));
  35. // Set up the ConfigImporter object for testing.
  36. $storage_comparer = new StorageComparer(
  37. $this->container->get('config.storage.sync'),
  38. $this->container->get('config.storage'),
  39. $this->container->get('config.manager')
  40. );
  41. $this->configImporter = new ConfigImporter(
  42. $storage_comparer->createChangelist(),
  43. $this->container->get('event_dispatcher'),
  44. $this->container->get('config.manager'),
  45. $this->container->get('lock'),
  46. $this->container->get('config.typed'),
  47. $this->container->get('module_handler'),
  48. $this->container->get('module_installer'),
  49. $this->container->get('theme_handler'),
  50. $this->container->get('string_translation')
  51. );
  52. }
  53. /**
  54. * Tests omission of module APIs for bare configuration operations.
  55. */
  56. public function testNoImport() {
  57. $dynamic_name = 'config_test.dynamic.dotted.default';
  58. // Verify the default configuration values exist.
  59. $config = $this->config($dynamic_name);
  60. $this->assertIdentical($config->get('id'), 'dotted.default');
  61. // Verify that a bare $this->config() does not involve module APIs.
  62. $this->assertFalse(isset($GLOBALS['hook_config_test']));
  63. }
  64. /**
  65. * Tests that trying to import from an empty sync configuration directory
  66. * fails.
  67. */
  68. public function testEmptyImportFails() {
  69. try {
  70. $this->container->get('config.storage.sync')->deleteAll();
  71. $this->configImporter->reset()->import();
  72. $this->fail('ConfigImporterException thrown, successfully stopping an empty import.');
  73. }
  74. catch (ConfigImporterException $e) {
  75. $this->pass('ConfigImporterException thrown, successfully stopping an empty import.');
  76. }
  77. }
  78. /**
  79. * Tests verification of site UUID before importing configuration.
  80. */
  81. public function testSiteUuidValidate() {
  82. $sync = \Drupal::service('config.storage.sync');
  83. // Create updated configuration object.
  84. $config_data = $this->config('system.site')->get();
  85. // Generate a new site UUID.
  86. $config_data['uuid'] = \Drupal::service('uuid')->generate();
  87. $sync->write('system.site', $config_data);
  88. try {
  89. $this->configImporter->reset()->import();
  90. $this->fail('ConfigImporterException not thrown, invalid import was not stopped due to mis-matching site UUID.');
  91. }
  92. catch (ConfigImporterException $e) {
  93. $this->assertEqual($e->getMessage(), 'There were errors validating the config synchronization.');
  94. $error_log = $this->configImporter->getErrors();
  95. $expected = ['Site UUID in source storage does not match the target storage.'];
  96. $this->assertEqual($expected, $error_log);
  97. }
  98. }
  99. /**
  100. * Tests deletion of configuration during import.
  101. */
  102. public function testDeleted() {
  103. $dynamic_name = 'config_test.dynamic.dotted.default';
  104. $storage = $this->container->get('config.storage');
  105. $sync = $this->container->get('config.storage.sync');
  106. // Verify the default configuration values exist.
  107. $config = $this->config($dynamic_name);
  108. $this->assertIdentical($config->get('id'), 'dotted.default');
  109. // Delete the file from the sync directory.
  110. $sync->delete($dynamic_name);
  111. // Import.
  112. $this->configImporter->reset()->import();
  113. // Verify the file has been removed.
  114. $this->assertIdentical($storage->read($dynamic_name), FALSE);
  115. $config = $this->config($dynamic_name);
  116. $this->assertIdentical($config->get('id'), NULL);
  117. // Verify that appropriate module API hooks have been invoked.
  118. $this->assertTrue(isset($GLOBALS['hook_config_test']['load']));
  119. $this->assertFalse(isset($GLOBALS['hook_config_test']['presave']));
  120. $this->assertFalse(isset($GLOBALS['hook_config_test']['insert']));
  121. $this->assertFalse(isset($GLOBALS['hook_config_test']['update']));
  122. $this->assertTrue(isset($GLOBALS['hook_config_test']['predelete']));
  123. $this->assertTrue(isset($GLOBALS['hook_config_test']['delete']));
  124. $this->assertFalse($this->configImporter->hasUnprocessedConfigurationChanges());
  125. $logs = $this->configImporter->getErrors();
  126. $this->assertEqual(count($logs), 0);
  127. }
  128. /**
  129. * Tests creation of configuration during import.
  130. */
  131. public function testNew() {
  132. $dynamic_name = 'config_test.dynamic.new';
  133. $storage = $this->container->get('config.storage');
  134. $sync = $this->container->get('config.storage.sync');
  135. // Verify the configuration to create does not exist yet.
  136. $this->assertIdentical($storage->exists($dynamic_name), FALSE, $dynamic_name . ' not found.');
  137. // Create new config entity.
  138. $original_dynamic_data = [
  139. 'uuid' => '30df59bd-7b03-4cf7-bb35-d42fc49f0651',
  140. 'langcode' => \Drupal::languageManager()->getDefaultLanguage()->getId(),
  141. 'status' => TRUE,
  142. 'dependencies' => [],
  143. 'id' => 'new',
  144. 'label' => 'New',
  145. 'weight' => 0,
  146. 'style' => '',
  147. 'size' => '',
  148. 'size_value' => '',
  149. 'protected_property' => '',
  150. ];
  151. $sync->write($dynamic_name, $original_dynamic_data);
  152. $this->assertIdentical($sync->exists($dynamic_name), TRUE, $dynamic_name . ' found.');
  153. // Import.
  154. $this->configImporter->reset()->import();
  155. // Verify the values appeared.
  156. $config = $this->config($dynamic_name);
  157. $this->assertIdentical($config->get('label'), $original_dynamic_data['label']);
  158. // Verify that appropriate module API hooks have been invoked.
  159. $this->assertFalse(isset($GLOBALS['hook_config_test']['load']));
  160. $this->assertTrue(isset($GLOBALS['hook_config_test']['presave']));
  161. $this->assertTrue(isset($GLOBALS['hook_config_test']['insert']));
  162. $this->assertFalse(isset($GLOBALS['hook_config_test']['update']));
  163. $this->assertFalse(isset($GLOBALS['hook_config_test']['predelete']));
  164. $this->assertFalse(isset($GLOBALS['hook_config_test']['delete']));
  165. // Verify that hook_config_import_steps_alter() can add steps to
  166. // configuration synchronization.
  167. $this->assertTrue(isset($GLOBALS['hook_config_test']['config_import_steps_alter']));
  168. // Verify that there is nothing more to import.
  169. $this->assertFalse($this->configImporter->hasUnprocessedConfigurationChanges());
  170. $logs = $this->configImporter->getErrors();
  171. $this->assertEqual(count($logs), 0);
  172. }
  173. /**
  174. * Tests that secondary writes are overwritten.
  175. */
  176. public function testSecondaryWritePrimaryFirst() {
  177. $name_primary = 'config_test.dynamic.primary';
  178. $name_secondary = 'config_test.dynamic.secondary';
  179. $sync = $this->container->get('config.storage.sync');
  180. $uuid = $this->container->get('uuid');
  181. $values_primary = [
  182. 'id' => 'primary',
  183. 'label' => 'Primary',
  184. 'weight' => 0,
  185. 'uuid' => $uuid->generate(),
  186. ];
  187. $sync->write($name_primary, $values_primary);
  188. $values_secondary = [
  189. 'id' => 'secondary',
  190. 'label' => 'Secondary Sync',
  191. 'weight' => 0,
  192. 'uuid' => $uuid->generate(),
  193. // Add a dependency on primary, to ensure that is synced first.
  194. 'dependencies' => [
  195. 'config' => [$name_primary],
  196. ]
  197. ];
  198. $sync->write($name_secondary, $values_secondary);
  199. // Import.
  200. $this->configImporter->reset()->import();
  201. $entity_storage = \Drupal::entityManager()->getStorage('config_test');
  202. $primary = $entity_storage->load('primary');
  203. $this->assertEqual($primary->id(), 'primary');
  204. $this->assertEqual($primary->uuid(), $values_primary['uuid']);
  205. $this->assertEqual($primary->label(), $values_primary['label']);
  206. $secondary = $entity_storage->load('secondary');
  207. $this->assertEqual($secondary->id(), 'secondary');
  208. $this->assertEqual($secondary->uuid(), $values_secondary['uuid']);
  209. $this->assertEqual($secondary->label(), $values_secondary['label']);
  210. $logs = $this->configImporter->getErrors();
  211. $this->assertEqual(count($logs), 1);
  212. $this->assertEqual($logs[0], SafeMarkup::format('Deleted and replaced configuration entity "@name"', ['@name' => $name_secondary]));
  213. }
  214. /**
  215. * Tests that secondary writes are overwritten.
  216. */
  217. public function testSecondaryWriteSecondaryFirst() {
  218. $name_primary = 'config_test.dynamic.primary';
  219. $name_secondary = 'config_test.dynamic.secondary';
  220. $sync = $this->container->get('config.storage.sync');
  221. $uuid = $this->container->get('uuid');
  222. $values_primary = [
  223. 'id' => 'primary',
  224. 'label' => 'Primary',
  225. 'weight' => 0,
  226. 'uuid' => $uuid->generate(),
  227. // Add a dependency on secondary, so that is synced first.
  228. 'dependencies' => [
  229. 'config' => [$name_secondary],
  230. ]
  231. ];
  232. $sync->write($name_primary, $values_primary);
  233. $values_secondary = [
  234. 'id' => 'secondary',
  235. 'label' => 'Secondary Sync',
  236. 'weight' => 0,
  237. 'uuid' => $uuid->generate(),
  238. ];
  239. $sync->write($name_secondary, $values_secondary);
  240. // Import.
  241. $this->configImporter->reset()->import();
  242. $entity_storage = \Drupal::entityManager()->getStorage('config_test');
  243. $primary = $entity_storage->load('primary');
  244. $this->assertEqual($primary->id(), 'primary');
  245. $this->assertEqual($primary->uuid(), $values_primary['uuid']);
  246. $this->assertEqual($primary->label(), $values_primary['label']);
  247. $secondary = $entity_storage->load('secondary');
  248. $this->assertEqual($secondary->id(), 'secondary');
  249. $this->assertEqual($secondary->uuid(), $values_secondary['uuid']);
  250. $this->assertEqual($secondary->label(), $values_secondary['label']);
  251. $logs = $this->configImporter->getErrors();
  252. $this->assertEqual(count($logs), 1);
  253. $this->assertEqual($logs[0], Html::escape("Unexpected error during import with operation create for $name_primary: 'config_test' entity with ID 'secondary' already exists."));
  254. }
  255. /**
  256. * Tests that secondary updates for deleted files work as expected.
  257. */
  258. public function testSecondaryUpdateDeletedDeleterFirst() {
  259. $name_deleter = 'config_test.dynamic.deleter';
  260. $name_deletee = 'config_test.dynamic.deletee';
  261. $name_other = 'config_test.dynamic.other';
  262. $storage = $this->container->get('config.storage');
  263. $sync = $this->container->get('config.storage.sync');
  264. $uuid = $this->container->get('uuid');
  265. $values_deleter = [
  266. 'id' => 'deleter',
  267. 'label' => 'Deleter',
  268. 'weight' => 0,
  269. 'uuid' => $uuid->generate(),
  270. ];
  271. $storage->write($name_deleter, $values_deleter);
  272. $values_deleter['label'] = 'Updated Deleter';
  273. $sync->write($name_deleter, $values_deleter);
  274. $values_deletee = [
  275. 'id' => 'deletee',
  276. 'label' => 'Deletee',
  277. 'weight' => 0,
  278. 'uuid' => $uuid->generate(),
  279. // Add a dependency on deleter, to make sure that is synced first.
  280. 'dependencies' => [
  281. 'config' => [$name_deleter],
  282. ]
  283. ];
  284. $storage->write($name_deletee, $values_deletee);
  285. $values_deletee['label'] = 'Updated Deletee';
  286. $sync->write($name_deletee, $values_deletee);
  287. // Ensure that import will continue after the error.
  288. $values_other = [
  289. 'id' => 'other',
  290. 'label' => 'Other',
  291. 'weight' => 0,
  292. 'uuid' => $uuid->generate(),
  293. // Add a dependency on deleter, to make sure that is synced first. This
  294. // will also be synced after the deletee due to alphabetical ordering.
  295. 'dependencies' => [
  296. 'config' => [$name_deleter],
  297. ]
  298. ];
  299. $storage->write($name_other, $values_other);
  300. $values_other['label'] = 'Updated other';
  301. $sync->write($name_other, $values_other);
  302. // Check update changelist order.
  303. $updates = $this->configImporter->reset()->getStorageComparer()->getChangelist('update');
  304. $expected = [
  305. $name_deleter,
  306. $name_deletee,
  307. $name_other,
  308. ];
  309. $this->assertSame($expected, $updates);
  310. // Import.
  311. $this->configImporter->import();
  312. $entity_storage = \Drupal::entityManager()->getStorage('config_test');
  313. $deleter = $entity_storage->load('deleter');
  314. $this->assertEqual($deleter->id(), 'deleter');
  315. $this->assertEqual($deleter->uuid(), $values_deleter['uuid']);
  316. $this->assertEqual($deleter->label(), $values_deleter['label']);
  317. // The deletee was deleted in
  318. // \Drupal\config_test\Entity\ConfigTest::postSave().
  319. $this->assertFalse($entity_storage->load('deletee'));
  320. $other = $entity_storage->load('other');
  321. $this->assertEqual($other->id(), 'other');
  322. $this->assertEqual($other->uuid(), $values_other['uuid']);
  323. $this->assertEqual($other->label(), $values_other['label']);
  324. $logs = $this->configImporter->getErrors();
  325. $this->assertEqual(count($logs), 1);
  326. $this->assertEqual($logs[0], SafeMarkup::format('Update target "@name" is missing.', ['@name' => $name_deletee]));
  327. }
  328. /**
  329. * Tests that secondary updates for deleted files work as expected.
  330. *
  331. * This test is completely hypothetical since we only support full
  332. * configuration tree imports. Therefore, any configuration updates that cause
  333. * secondary deletes should be reflected already in the staged configuration.
  334. */
  335. public function testSecondaryUpdateDeletedDeleteeFirst() {
  336. $name_deleter = 'config_test.dynamic.deleter';
  337. $name_deletee = 'config_test.dynamic.deletee';
  338. $storage = $this->container->get('config.storage');
  339. $sync = $this->container->get('config.storage.sync');
  340. $uuid = $this->container->get('uuid');
  341. $values_deleter = [
  342. 'id' => 'deleter',
  343. 'label' => 'Deleter',
  344. 'weight' => 0,
  345. 'uuid' => $uuid->generate(),
  346. // Add a dependency on deletee, to make sure that is synced first.
  347. 'dependencies' => [
  348. 'config' => [$name_deletee],
  349. ],
  350. ];
  351. $storage->write($name_deleter, $values_deleter);
  352. $values_deleter['label'] = 'Updated Deleter';
  353. $sync->write($name_deleter, $values_deleter);
  354. $values_deletee = [
  355. 'id' => 'deletee',
  356. 'label' => 'Deletee',
  357. 'weight' => 0,
  358. 'uuid' => $uuid->generate(),
  359. ];
  360. $storage->write($name_deletee, $values_deletee);
  361. $values_deletee['label'] = 'Updated Deletee';
  362. $sync->write($name_deletee, $values_deletee);
  363. // Import.
  364. $this->configImporter->reset()->import();
  365. $entity_storage = \Drupal::entityManager()->getStorage('config_test');
  366. // Both entities are deleted. ConfigTest::postSave() causes updates of the
  367. // deleter entity to delete the deletee entity. Since the deleter depends on
  368. // the deletee, removing the deletee causes the deleter to be removed.
  369. $this->assertFalse($entity_storage->load('deleter'));
  370. $this->assertFalse($entity_storage->load('deletee'));
  371. $logs = $this->configImporter->getErrors();
  372. $this->assertEqual(count($logs), 0);
  373. }
  374. /**
  375. * Tests that secondary deletes for deleted files work as expected.
  376. */
  377. public function testSecondaryDeletedDeleteeSecond() {
  378. $name_deleter = 'config_test.dynamic.deleter';
  379. $name_deletee = 'config_test.dynamic.deletee';
  380. $storage = $this->container->get('config.storage');
  381. $uuid = $this->container->get('uuid');
  382. $values_deleter = [
  383. 'id' => 'deleter',
  384. 'label' => 'Deleter',
  385. 'weight' => 0,
  386. 'uuid' => $uuid->generate(),
  387. // Add a dependency on deletee, to make sure this delete is synced first.
  388. 'dependencies' => [
  389. 'config' => [$name_deletee],
  390. ],
  391. ];
  392. $storage->write($name_deleter, $values_deleter);
  393. $values_deletee = [
  394. 'id' => 'deletee',
  395. 'label' => 'Deletee',
  396. 'weight' => 0,
  397. 'uuid' => $uuid->generate(),
  398. ];
  399. $storage->write($name_deletee, $values_deletee);
  400. // Import.
  401. $this->configImporter->reset()->import();
  402. $entity_storage = \Drupal::entityManager()->getStorage('config_test');
  403. $this->assertFalse($entity_storage->load('deleter'));
  404. $this->assertFalse($entity_storage->load('deletee'));
  405. // The deletee entity does not exist as the delete worked and although the
  406. // delete occurred in \Drupal\config_test\Entity\ConfigTest::postDelete()
  407. // this does not matter.
  408. $logs = $this->configImporter->getErrors();
  409. $this->assertEqual(count($logs), 0);
  410. }
  411. /**
  412. * Tests updating of configuration during import.
  413. */
  414. public function testUpdated() {
  415. $name = 'config_test.system';
  416. $dynamic_name = 'config_test.dynamic.dotted.default';
  417. $storage = $this->container->get('config.storage');
  418. $sync = $this->container->get('config.storage.sync');
  419. // Verify that the configuration objects to import exist.
  420. $this->assertIdentical($storage->exists($name), TRUE, $name . ' found.');
  421. $this->assertIdentical($storage->exists($dynamic_name), TRUE, $dynamic_name . ' found.');
  422. // Replace the file content of the existing configuration objects in the
  423. // sync directory.
  424. $original_name_data = [
  425. 'foo' => 'beer',
  426. ];
  427. $sync->write($name, $original_name_data);
  428. $original_dynamic_data = $storage->read($dynamic_name);
  429. $original_dynamic_data['label'] = 'Updated';
  430. $sync->write($dynamic_name, $original_dynamic_data);
  431. // Verify the active configuration still returns the default values.
  432. $config = $this->config($name);
  433. $this->assertIdentical($config->get('foo'), 'bar');
  434. $config = $this->config($dynamic_name);
  435. $this->assertIdentical($config->get('label'), 'Default');
  436. // Import.
  437. $this->configImporter->reset()->import();
  438. // Verify the values were updated.
  439. \Drupal::configFactory()->reset($name);
  440. $config = $this->config($name);
  441. $this->assertIdentical($config->get('foo'), 'beer');
  442. $config = $this->config($dynamic_name);
  443. $this->assertIdentical($config->get('label'), 'Updated');
  444. // Verify that the original file content is still the same.
  445. $this->assertIdentical($sync->read($name), $original_name_data);
  446. $this->assertIdentical($sync->read($dynamic_name), $original_dynamic_data);
  447. // Verify that appropriate module API hooks have been invoked.
  448. $this->assertTrue(isset($GLOBALS['hook_config_test']['load']));
  449. $this->assertTrue(isset($GLOBALS['hook_config_test']['presave']));
  450. $this->assertFalse(isset($GLOBALS['hook_config_test']['insert']));
  451. $this->assertTrue(isset($GLOBALS['hook_config_test']['update']));
  452. $this->assertFalse(isset($GLOBALS['hook_config_test']['predelete']));
  453. $this->assertFalse(isset($GLOBALS['hook_config_test']['delete']));
  454. // Verify that there is nothing more to import.
  455. $this->assertFalse($this->configImporter->hasUnprocessedConfigurationChanges());
  456. $logs = $this->configImporter->getErrors();
  457. $this->assertEqual(count($logs), 0);
  458. }
  459. /**
  460. * Tests the isInstallable method()
  461. */
  462. public function testIsInstallable() {
  463. $config_name = 'config_test.dynamic.isinstallable';
  464. $this->assertFalse($this->container->get('config.storage')->exists($config_name));
  465. \Drupal::state()->set('config_test.isinstallable', TRUE);
  466. $this->installConfig(['config_test']);
  467. $this->assertTrue($this->container->get('config.storage')->exists($config_name));
  468. }
  469. /**
  470. * Tests dependency validation during configuration import.
  471. *
  472. * @see \Drupal\Core\EventSubscriber\ConfigImportSubscriber
  473. * @see \Drupal\Core\Config\ConfigImporter::createExtensionChangelist()
  474. */
  475. public function testUnmetDependency() {
  476. $storage = $this->container->get('config.storage');
  477. $sync = $this->container->get('config.storage.sync');
  478. // Test an unknown configuration owner.
  479. $sync->write('unknown.config', ['test' => 'test']);
  480. // Make a config entity have unmet dependencies.
  481. $config_entity_data = $sync->read('config_test.dynamic.dotted.default');
  482. $config_entity_data['dependencies'] = ['module' => ['unknown']];
  483. $sync->write('config_test.dynamic.dotted.module', $config_entity_data);
  484. $config_entity_data['dependencies'] = ['theme' => ['unknown']];
  485. $sync->write('config_test.dynamic.dotted.theme', $config_entity_data);
  486. $config_entity_data['dependencies'] = ['config' => ['unknown']];
  487. $sync->write('config_test.dynamic.dotted.config', $config_entity_data);
  488. // Make an active config depend on something that is missing in sync.
  489. // The whole configuration needs to be consistent, not only the updated one.
  490. $config_entity_data['dependencies'] = [];
  491. $storage->write('config_test.dynamic.dotted.deleted', $config_entity_data);
  492. $config_entity_data['dependencies'] = ['config' => ['config_test.dynamic.dotted.deleted']];
  493. $storage->write('config_test.dynamic.dotted.existing', $config_entity_data);
  494. $sync->write('config_test.dynamic.dotted.existing', $config_entity_data);
  495. $extensions = $sync->read('core.extension');
  496. // Add a module and a theme that do not exist.
  497. $extensions['module']['unknown_module'] = 0;
  498. $extensions['theme']['unknown_theme'] = 0;
  499. // Add a module and a theme that depend on uninstalled extensions.
  500. $extensions['module']['book'] = 0;
  501. $extensions['theme']['bartik'] = 0;
  502. $sync->write('core.extension', $extensions);
  503. try {
  504. $this->configImporter->reset()->import();
  505. $this->fail('ConfigImporterException not thrown; an invalid import was not stopped due to missing dependencies.');
  506. }
  507. catch (ConfigImporterException $e) {
  508. $this->assertEqual($e->getMessage(), 'There were errors validating the config synchronization.');
  509. $error_log = $this->configImporter->getErrors();
  510. $expected = [
  511. 'Unable to install the <em class="placeholder">unknown_module</em> module since it does not exist.',
  512. 'Unable to install the <em class="placeholder">Book</em> module since it requires the <em class="placeholder">Node, Text, Field, Filter, User</em> modules.',
  513. 'Unable to install the <em class="placeholder">unknown_theme</em> theme since it does not exist.',
  514. 'Unable to install the <em class="placeholder">Bartik</em> theme since it requires the <em class="placeholder">Classy</em> theme.',
  515. 'Configuration <em class="placeholder">config_test.dynamic.dotted.config</em> depends on the <em class="placeholder">unknown</em> configuration that will not exist after import.',
  516. 'Configuration <em class="placeholder">config_test.dynamic.dotted.existing</em> depends on the <em class="placeholder">config_test.dynamic.dotted.deleted</em> configuration that will not exist after import.',
  517. 'Configuration <em class="placeholder">config_test.dynamic.dotted.module</em> depends on the <em class="placeholder">unknown</em> module that will not be installed after import.',
  518. 'Configuration <em class="placeholder">config_test.dynamic.dotted.theme</em> depends on the <em class="placeholder">unknown</em> theme that will not be installed after import.',
  519. 'Configuration <em class="placeholder">unknown.config</em> depends on the <em class="placeholder">unknown</em> extension that will not be installed after import.',
  520. ];
  521. foreach ($expected as $expected_message) {
  522. $this->assertTrue(in_array($expected_message, $error_log), $expected_message);
  523. }
  524. }
  525. // Make a config entity have mulitple unmet dependencies.
  526. $config_entity_data = $sync->read('config_test.dynamic.dotted.default');
  527. $config_entity_data['dependencies'] = ['module' => ['unknown', 'dblog']];
  528. $sync->write('config_test.dynamic.dotted.module', $config_entity_data);
  529. $config_entity_data['dependencies'] = ['theme' => ['unknown', 'seven']];
  530. $sync->write('config_test.dynamic.dotted.theme', $config_entity_data);
  531. $config_entity_data['dependencies'] = ['config' => ['unknown', 'unknown2']];
  532. $sync->write('config_test.dynamic.dotted.config', $config_entity_data);
  533. try {
  534. $this->configImporter->reset()->import();
  535. $this->fail('ConfigImporterException not thrown, invalid import was not stopped due to missing dependencies.');
  536. }
  537. catch (ConfigImporterException $e) {
  538. $this->assertEqual($e->getMessage(), 'There were errors validating the config synchronization.');
  539. $error_log = $this->configImporter->getErrors();
  540. $expected = [
  541. 'Configuration <em class="placeholder">config_test.dynamic.dotted.config</em> depends on configuration (<em class="placeholder">unknown, unknown2</em>) that will not exist after import.',
  542. 'Configuration <em class="placeholder">config_test.dynamic.dotted.module</em> depends on modules (<em class="placeholder">unknown, Database Logging</em>) that will not be installed after import.',
  543. 'Configuration <em class="placeholder">config_test.dynamic.dotted.theme</em> depends on themes (<em class="placeholder">unknown, Seven</em>) that will not be installed after import.',
  544. ];
  545. foreach ($expected as $expected_message) {
  546. $this->assertTrue(in_array($expected_message, $error_log), $expected_message);
  547. }
  548. }
  549. }
  550. /**
  551. * Tests missing core.extension during configuration import.
  552. *
  553. * @see \Drupal\Core\EventSubscriber\ConfigImportSubscriber
  554. */
  555. public function testMissingCoreExtension() {
  556. $sync = $this->container->get('config.storage.sync');
  557. $sync->delete('core.extension');
  558. try {
  559. $this->configImporter->reset()->import();
  560. $this->fail('ConfigImporterException not thrown, invalid import was not stopped due to missing dependencies.');
  561. }
  562. catch (ConfigImporterException $e) {
  563. $this->assertEqual($e->getMessage(), 'There were errors validating the config synchronization.');
  564. $error_log = $this->configImporter->getErrors();
  565. $this->assertEqual(['The core.extension configuration does not exist.'], $error_log);
  566. }
  567. }
  568. /**
  569. * Tests install profile validation during configuration import.
  570. *
  571. * @see \Drupal\Core\EventSubscriber\ConfigImportSubscriber
  572. */
  573. public function testInstallProfile() {
  574. $sync = $this->container->get('config.storage.sync');
  575. $extensions = $sync->read('core.extension');
  576. // Add an install profile.
  577. $extensions['module']['standard'] = 0;
  578. $sync->write('core.extension', $extensions);
  579. try {
  580. $this->configImporter->reset()->import();
  581. $this->fail('ConfigImporterException not thrown; an invalid import was not stopped due to missing dependencies.');
  582. }
  583. catch (ConfigImporterException $e) {
  584. $this->assertEqual($e->getMessage(), 'There were errors validating the config synchronization.');
  585. $error_log = $this->configImporter->getErrors();
  586. // Install profiles should not even be scanned at this point.
  587. $this->assertEqual(['Unable to install the <em class="placeholder">standard</em> module since it does not exist.'], $error_log);
  588. }
  589. }
  590. /**
  591. * Tests install profile validation during configuration import.
  592. *
  593. * @see \Drupal\Core\EventSubscriber\ConfigImportSubscriber
  594. */
  595. public function testInstallProfileMisMatch() {
  596. $sync = $this->container->get('config.storage.sync');
  597. $extensions = $sync->read('core.extension');
  598. // Change the install profile.
  599. $extensions['profile'] = 'this_will_not_work';
  600. $sync->write('core.extension', $extensions);
  601. try {
  602. $this->configImporter->reset()->import();
  603. $this->fail('ConfigImporterException not thrown; an invalid import was not stopped due to missing dependencies.');
  604. }
  605. catch (ConfigImporterException $e) {
  606. $this->assertEqual($e->getMessage(), 'There were errors validating the config synchronization.');
  607. $error_log = $this->configImporter->getErrors();
  608. // Install profiles can not be changed. Note that KernelTestBase currently
  609. // does not use an install profile. This situation should be impossible
  610. // to get in but site's can removed the install profile setting from
  611. // settings.php so the test is valid.
  612. $this->assertEqual(['Cannot change the install profile from <em class="placeholder">this_will_not_work</em> to <em class="placeholder"></em> once Drupal is installed.'], $error_log);
  613. }
  614. }
  615. /**
  616. * Tests config_get_config_directory().
  617. */
  618. public function testConfigGetConfigDirectory() {
  619. global $config_directories;
  620. $directory = config_get_config_directory(CONFIG_SYNC_DIRECTORY);
  621. $this->assertEqual($config_directories[CONFIG_SYNC_DIRECTORY], $directory);
  622. $message = 'Calling config_get_config_directory() with CONFIG_ACTIVE_DIRECTORY results in an exception.';
  623. try {
  624. config_get_config_directory(CONFIG_ACTIVE_DIRECTORY);
  625. $this->fail($message);
  626. }
  627. catch (\Exception $e) {
  628. $this->pass($message);
  629. }
  630. }
  631. /**
  632. * Tests the isSyncing flags.
  633. */
  634. public function testIsSyncingInHooks() {
  635. $dynamic_name = 'config_test.dynamic.dotted.default';
  636. $storage = $this->container->get('config.storage');
  637. // Verify the default configuration values exist.
  638. $config = $this->config($dynamic_name);
  639. $this->assertSame('dotted.default', $config->get('id'));
  640. // Delete the config so that create hooks will fire.
  641. $storage->delete($dynamic_name);
  642. \Drupal::state()->set('config_test.store_isSyncing', []);
  643. $this->configImporter->reset()->import();
  644. // The values of the syncing values should be stored in state by
  645. // config_test_config_test_create().
  646. $state = \Drupal::state()->get('config_test.store_isSyncing');
  647. $this->assertTrue($state['global_state::create'], '\Drupal::isConfigSyncing() returns TRUE');
  648. $this->assertTrue($state['entity_state::create'], 'ConfigEntity::isSyncing() returns TRUE');
  649. $this->assertTrue($state['global_state::presave'], '\Drupal::isConfigSyncing() returns TRUE');
  650. $this->assertTrue($state['entity_state::presave'], 'ConfigEntity::isSyncing() returns TRUE');
  651. $this->assertTrue($state['global_state::insert'], '\Drupal::isConfigSyncing() returns TRUE');
  652. $this->assertTrue($state['entity_state::insert'], 'ConfigEntity::isSyncing() returns TRUE');
  653. // Cause a config update so update hooks will fire.
  654. $config = $this->config($dynamic_name);
  655. $config->set('label', 'A new name')->save();
  656. \Drupal::state()->set('config_test.store_isSyncing', []);
  657. $this->configImporter->reset()->import();
  658. // The values of the syncing values should be stored in state by
  659. // config_test_config_test_create().
  660. $state = \Drupal::state()->get('config_test.store_isSyncing');
  661. $this->assertTrue($state['global_state::presave'], '\Drupal::isConfigSyncing() returns TRUE');
  662. $this->assertTrue($state['entity_state::presave'], 'ConfigEntity::isSyncing() returns TRUE');
  663. $this->assertTrue($state['global_state::update'], '\Drupal::isConfigSyncing() returns TRUE');
  664. $this->assertTrue($state['entity_state::update'], 'ConfigEntity::isSyncing() returns TRUE');
  665. // Cause a config delete so delete hooks will fire.
  666. $sync = $this->container->get('config.storage.sync');
  667. $sync->delete($dynamic_name);
  668. \Drupal::state()->set('config_test.store_isSyncing', []);
  669. $this->configImporter->reset()->import();
  670. // The values of the syncing values should be stored in state by
  671. // config_test_config_test_create().
  672. $state = \Drupal::state()->get('config_test.store_isSyncing');
  673. $this->assertTrue($state['global_state::predelete'], '\Drupal::isConfigSyncing() returns TRUE');
  674. $this->assertTrue($state['entity_state::predelete'], 'ConfigEntity::isSyncing() returns TRUE');
  675. $this->assertTrue($state['global_state::delete'], '\Drupal::isConfigSyncing() returns TRUE');
  676. $this->assertTrue($state['entity_state::delete'], 'ConfigEntity::isSyncing() returns TRUE');
  677. }
  678. /**
  679. * Tests that the isConfigSyncing flag is cleanup after an invalid step.
  680. */
  681. public function testInvalidStep() {
  682. $this->assertFalse(\Drupal::isConfigSyncing(), 'Before an import \Drupal::isConfigSyncing() returns FALSE');
  683. $context = [];
  684. try {
  685. $this->configImporter->doSyncStep('a_non_existent_step', $context);
  686. $this->fail('Expected \InvalidArgumentException thrown');
  687. }
  688. catch (\InvalidArgumentException $e) {
  689. $this->pass('Expected \InvalidArgumentException thrown');
  690. }
  691. $this->assertFalse(\Drupal::isConfigSyncing(), 'After an invalid step \Drupal::isConfigSyncing() returns FALSE');
  692. }
  693. /**
  694. * Tests that the isConfigSyncing flag is set correctly during a custom step.
  695. */
  696. public function testCustomStep() {
  697. $this->assertFalse(\Drupal::isConfigSyncing(), 'Before an import \Drupal::isConfigSyncing() returns FALSE');
  698. $context = [];
  699. $this->configImporter->doSyncStep([self::class, 'customStep'], $context);
  700. $this->assertTrue($context['is_syncing'], 'Inside a custom step \Drupal::isConfigSyncing() returns TRUE');
  701. $this->assertFalse(\Drupal::isConfigSyncing(), 'After an valid custom step \Drupal::isConfigSyncing() returns FALSE');
  702. }
  703. /**
  704. * Helper meothd to test custom config installer steps.
  705. *
  706. * @param array $context
  707. * Batch context.
  708. * @param \Drupal\Core\Config\ConfigImporter $importer
  709. * The config importer.
  710. */
  711. public static function customStep(array &$context, ConfigImporter $importer) {
  712. $context['is_syncing'] = \Drupal::isConfigSyncing();
  713. }
  714. }