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