diff --git a/core/lib/Drupal/Core/Config/Schema/SchemaCheckTrait.php b/core/lib/Drupal/Core/Config/Schema/SchemaCheckTrait.php index 0fccd47..5127adc 100644 --- a/core/lib/Drupal/Core/Config/Schema/SchemaCheckTrait.php +++ b/core/lib/Drupal/Core/Config/Schema/SchemaCheckTrait.php @@ -81,7 +81,7 @@ protected function checkValue($key, $value) { $error_key = $this->configName . ':' . $key; $element = $this->schema->get($key); if ($element instanceof Undefined) { - return array($error_key => 'Missing schema.'); + return array($error_key => 'missing schema'); } // Do not check value if it is defined to be ignored. @@ -104,13 +104,13 @@ protected function checkValue($key, $value) { } $class = get_class($element); if (!$success) { - return array($error_key => "Variable type is $type but applied schema class is $class."); + return array($error_key => "variable type is $type but applied schema class is $class"); } } else { $errors = array(); if (!$element instanceof TraversableTypedDataInterface) { - $errors[$error_key] = 'Non-scalar value but not defined as an array (such as mapping or sequence).'; + $errors[$error_key] = 'non-scalar value but not defined as an array (such as mapping or sequence)'; } // Go on processing so we can get errors on all levels. Any non-scalar diff --git a/core/lib/Drupal/Core/Config/Testing/ConfigSchemaChecker.php b/core/lib/Drupal/Core/Config/Testing/ConfigSchemaChecker.php new file mode 100644 index 0000000..db80944 --- /dev/null +++ b/core/lib/Drupal/Core/Config/Testing/ConfigSchemaChecker.php @@ -0,0 +1,93 @@ +typedManager = $typed_manager; + } + + /** + * Checks that configuration complies with its schema on config save. + * + * @param \Drupal\Core\Config\ConfigCrudEvent $event + * The configuration event. + * + * @throws \Drupal\Core\Config\Schema\SchemaIncompleteException + * Exception thrown when configuration does not match its schema. + */ + public function onConfigSave(ConfigCrudEvent $event) { + $saved_config = $event->getConfig(); + $name = $saved_config->getName(); + $data = $saved_config->get(); + $checksum = crc32(serialize($data)); + if (!isset($this->checked[$name . ':' . $checksum])) { + $this->checked[$name . ':' . $checksum] = TRUE; + $errors = $this->checkConfigSchema($this->typedManager, $name, $data); + if ($errors === FALSE) { + throw new SchemaIncompleteException(String::format('No schema for @config_name', array('@config_name' => $name))); + } + elseif (is_array($errors)) { + $text_errors = []; + foreach ($errors as $key => $error) { + $text_errors[] = String::format('@key @error', array('@key' => $key, '@error' => $error)); + } + throw new SchemaIncompleteException(String::format('Schema errors for @config_name with the following errors: @errors', array('@config_name' => $name, '@errors' => implode(', ', $text_errors)))); + } + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events[ConfigEvents::SAVE][] = array('onConfigSave', 255); + return $events; + } + +} diff --git a/core/modules/config/src/Tests/SchemaCheckTestTrait.php b/core/modules/config/src/Tests/SchemaCheckTestTrait.php index c5b4cba..3a88296 100644 --- a/core/modules/config/src/Tests/SchemaCheckTestTrait.php +++ b/core/modules/config/src/Tests/SchemaCheckTestTrait.php @@ -45,7 +45,7 @@ public function assertConfigSchema(TypedConfigManagerInterface $typed_config, $c foreach ($errors as $key => $error) { // @todo Since the use of this trait is under TestBase, it works. // Can be fixed as part of https://drupal.org/node/2260053. - $this->fail($key . ': ' . $error); + $this->fail(String::format('Schema key @key failed with: @error', array('@key' => $key, '@error' => $error))); } } } diff --git a/core/modules/config/src/Tests/SchemaCheckTraitTest.php b/core/modules/config/src/Tests/SchemaCheckTraitTest.php index 890eed8..450d4f1 100644 --- a/core/modules/config/src/Tests/SchemaCheckTraitTest.php +++ b/core/modules/config/src/Tests/SchemaCheckTraitTest.php @@ -61,9 +61,9 @@ public function testTrait() { $config_data['boolean'] = array(); $ret = $this->checkConfigSchema($this->typedConfig, 'config_test.types', $config_data); $expected = array( - 'config_test.types:new_key' => 'Missing schema.', - 'config_test.types:new_array' => 'Missing schema.', - 'config_test.types:boolean' => 'Non-scalar value but not defined as an array (such as mapping or sequence).', + 'config_test.types:new_key' => 'missing schema', + 'config_test.types:new_array' => 'missing schema', + 'config_test.types:boolean' => 'non-scalar value but not defined as an array (such as mapping or sequence)', ); $this->assertIdentical($ret, $expected); } diff --git a/core/modules/config/src/Tests/SchemaConfigListenerTest.php b/core/modules/config/src/Tests/SchemaConfigListenerTest.php new file mode 100644 index 0000000..84bf60e --- /dev/null +++ b/core/modules/config/src/Tests/SchemaConfigListenerTest.php @@ -0,0 +1,72 @@ +set('foo', 'bar')->save(); + $this->fail($message); + } + catch (SchemaIncompleteException $e) { + $this->pass($message); + $this->assertEqual('No schema for config_schema_test.noschema', $e->getMessage()); + } + + // Test a valid schema. + $message = 'Unexpected SchemaIncompleteException thrown'; + $config = \Drupal::config('config_test.types')->set('int', 10); + try { + $config->save(); + $this->pass($message); + } + catch (SchemaIncompleteException $e) { + $this->fail($message); + } + + // Test an invalid schema. + $message = 'Expected SchemaIncompleteException thrown'; + $config = \Drupal::config('config_test.types') + ->set('foo', 'bar') + ->set('array', 1); + try { + $config->save(); + $this->fail($message); + } + catch (SchemaIncompleteException $e) { + $this->pass($message); + $this->assertEqual('Schema errors for config_test.types with the following errors: config_test.types:foo missing schema, config_test.types:array variable type is integer but applied schema class is Drupal\Core\Config\Schema\Sequence', $e->getMessage()); + } + } + +} diff --git a/core/modules/config/src/Tests/SchemaConfigListenerWebTest.php b/core/modules/config/src/Tests/SchemaConfigListenerWebTest.php new file mode 100644 index 0000000..10ddc7b --- /dev/null +++ b/core/modules/config/src/Tests/SchemaConfigListenerWebTest.php @@ -0,0 +1,77 @@ +set('foo', 'bar')->save(); + $this->fail($msg); + } + catch (SchemaIncompleteException $e) { + $this->pass($msg); + $this->assertEqual('No schema for config_schema_test.noschema', $e->getMessage()); + } + + // Test a valid schema. + $msg = 'Unexpected SchemaIncompleteException thrown'; + $config = \Drupal::config('config_test.types')->set('int', 10); + try { + $config->save(); + $this->pass($msg); + } + catch (SchemaIncompleteException $e) { + $this->fail($msg); + } + + // Test an invalid schema. + $msg = 'Expected SchemaIncompleteException thrown'; + $config = \Drupal::config('config_test.types') + ->set('foo', 'bar') + ->set('array', 1); + try { + $config->save(); + $this->fail($msg); + } + catch (SchemaIncompleteException $e) { + $this->pass($msg); + $this->assertEqual('Schema errors for config_test.types with the following errors: config_test.types:array variable type is integer but applied schema class is Drupal\Core\Config\Schema\Sequence, config_test.types:foo missing schema', $e->getMessage()); + } + + // Test that the config event listener is working in the child site. + $this->drupalGet('config_test/schema_listener'); + $this->assertText('No schema for config_schema_test.noschema'); + } + +} diff --git a/core/modules/config/tests/config_test/config_test.routing.yml b/core/modules/config/tests/config_test/config_test.routing.yml index 4873d20..f536326 100644 --- a/core/modules/config/tests/config_test/config_test.routing.yml +++ b/core/modules/config/tests/config_test/config_test.routing.yml @@ -58,3 +58,10 @@ entity.config_test.delete_form_config_test_no_status: _entity_form: 'config_test_no_status.delete' requirements: _access: 'TRUE' + +config_test.schema_listener: + path: '/config_test/schema_listener' + defaults: + _content: '\Drupal\config_test\SchemaListenerController::test' + requirements: + _access: 'TRUE' diff --git a/core/modules/config/tests/config_test/src/SchemaListenerController.php b/core/modules/config/tests/config_test/src/SchemaListenerController.php new file mode 100644 index 0000000..fe91ade --- /dev/null +++ b/core/modules/config/tests/config_test/src/SchemaListenerController.php @@ -0,0 +1,32 @@ +config('config_schema_test.noschema')->set('foo', 'bar')->save(); + } + catch (SchemaIncompleteException $e) { + return [ + '#markup' => $e->getMessage(), + ]; + } + } + +} diff --git a/core/modules/simpletest/src/KernelTestBase.php b/core/modules/simpletest/src/KernelTestBase.php index e281988..d947caa 100644 --- a/core/modules/simpletest/src/KernelTestBase.php +++ b/core/modules/simpletest/src/KernelTestBase.php @@ -263,6 +263,13 @@ public function containerBuild(ContainerBuilder $container) { ->addArgument(Database::getConnection()) ->addArgument('config'); + if ($this->strictConfigSchema) { + $container + ->register('simpletest.config_schema_checker', 'Drupal\Core\Config\Testing\ConfigSchemaChecker') + ->addArgument(new Reference('config.typed')) + ->addTag('event_subscriber'); + } + $keyvalue_options = $container->getParameter('factory.keyvalue') ?: array(); $keyvalue_options['default'] = 'keyvalue.memory'; $container->setParameter('factory.keyvalue', $keyvalue_options); diff --git a/core/modules/simpletest/src/TestBase.php b/core/modules/simpletest/src/TestBase.php index 9475715..e822adb 100644 --- a/core/modules/simpletest/src/TestBase.php +++ b/core/modules/simpletest/src/TestBase.php @@ -203,6 +203,15 @@ protected $originalSessionName; /** + * Set to TRUE to strict check all configuration saved. + * + * @see \Drupal\Core\Config\Testing\ConfigSchemaChecker + * + * @var bool + */ + protected $strictConfigSchema = FALSE; + + /** * Constructor for Test. * * @param $test_id diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php index 730f84f..a8568d6 100644 --- a/core/modules/simpletest/src/WebTestBase.php +++ b/core/modules/simpletest/src/WebTestBase.php @@ -821,7 +821,17 @@ protected function setUp() { // Copy the testing-specific service overrides in place. copy($settings_services_file, $directory . '/services.yml'); } - + if ($this->strictConfigSchema) { + // Add a listener to validate configuration schema on save. + $yaml = new \Symfony\Component\Yaml\Yaml(); + $services = $yaml->parse($directory . '/services.yml'); + $services['services']['simpletest.config_schema_checker'] = [ + 'class' => 'Drupal\Core\Config\Testing\ConfigSchemaChecker', + 'arguments' => ['@config.typed'], + 'tags' => [['name' => 'event_subscriber']] + ]; + file_put_contents($directory . '/services.yml', $yaml->dump($services)); + } // Since Drupal is bootstrapped already, install_begin_request() will not // bootstrap into DRUPAL_BOOTSTRAP_CONFIGURATION (again). Hence, we have to // reload the newly written custom settings.php manually. diff --git a/core/modules/system/src/Tests/Menu/MenuTreeStorageTest.php b/core/modules/system/src/Tests/Menu/MenuTreeStorageTest.php index a923e9b..39c403c 100644 --- a/core/modules/system/src/Tests/Menu/MenuTreeStorageTest.php +++ b/core/modules/system/src/Tests/Menu/MenuTreeStorageTest.php @@ -307,15 +307,15 @@ public function testLoadByProperties() { array('foo' => 'bar'), array(0 => 'wrong'), ); - $msg = 'An invalid property name throws an exception.'; + $message = 'An invalid property name throws an exception.'; foreach ($tests as $properties) { try { $this->treeStorage->loadByProperties($properties); - $this->fail($msg); + $this->fail($message); } catch (\InvalidArgumentException $e) { $this->assertTrue(preg_match('/^An invalid property name, .+ was specified. Allowed property names are:/', $e->getMessage()), 'Found expected exception message.'); - $this->pass($msg); + $this->pass($message); } } $this->addMenuLink('test_link.1', '', 'test', array(), 'menu1');