Commit 121bbc14 authored by catch's avatar catch

Issue #2371843 by alexpott: Add event listener to check schema on config save.

parent 15ddad3b
......@@ -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
......
<?php
/**
* @file
* Contains \Drupal\Core\Config\Testing\ConfigSchemaChecker.
*/
namespace Drupal\Core\Config\Testing;
use Drupal\Component\Utility\String;
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Drupal\Core\Config\Schema\SchemaCheckTrait;
use Drupal\Core\Config\Schema\SchemaIncompleteException;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Listens to the config save event and validates schema.
*
* If tests have the $strictConfigSchema property set to TRUE this event
* listener will be added to the container and throw exceptions if configuration
* is invalid.
*
* @see \Drupal\simpletest\WebTestBase::setUp()
* @see \Drupal\simpletest\KernelTestBase::containerBuild()
*/
class ConfigSchemaChecker implements EventSubscriberInterface {
use SchemaCheckTrait;
/**
* The typed config manger.
*
* @var \Drupal\Core\Config\TypedConfigManagerInterface
*/
protected $typedManager;
/**
* An array of config checked already. Keyed by config name and a checksum.
*
* @var array
*/
protected $checked = array();
/**
* Constructs the ConfigSchemaChecker object.
*
* @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_manager
* The typed config manager.
*/
public function __construct(TypedConfigManagerInterface $typed_manager) {
$this->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;
}
}
......@@ -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)));
}
}
}
......
......@@ -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);
}
......
<?php
/**
* @file
* Contains \Drupal\config\Tests\SchemaConfigListenerTest.
*/
namespace Drupal\config\Tests;
use Drupal\Core\Config\Schema\SchemaCheckTrait;
use Drupal\Core\Config\Schema\SchemaIncompleteException;
use Drupal\simpletest\KernelTestBase;
/**
* Tests the functionality of ConfigSchemaChecker in KernelTestBase tests.
*
* @group config
*/
class SchemaConfigListenerTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = array('config_test');
/**
* {@inheritdoc}
*/
protected $strictConfigSchema = TRUE;
/**
* Tests \Drupal\Core\Config\Testing\ConfigSchemaChecker.
*/
public function testConfigSchemaChecker() {
// Test a non-existing schema.
$message = 'Expected SchemaIncompleteException thrown';
try {
\Drupal::config('config_schema_test.noschema')->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());
}
}
}
<?php
/**
* @file
* Contains \Drupal\config\Tests\SchemaConfigListenerWebTest.
*/
namespace Drupal\config\Tests;
use Drupal\Core\Config\Schema\SchemaCheckTrait;
use Drupal\Core\Config\Schema\SchemaIncompleteException;
use Drupal\simpletest\KernelTestBase;
use Drupal\simpletest\WebTestBase;
/**
* Tests the functionality of ConfigSchemaChecker in WebTestBase tests.
*
* @group config
*/
class SchemaConfigListenerWebTest extends WebTestBase {
/**
* {@inheritdoc}
*/
public static $modules = array('config_test');
/**
* {@inheritdoc}
*/
protected $strictConfigSchema = TRUE;
/**
* Tests \Drupal\Core\Config\Testing\ConfigSchemaChecker.
*/
public function testConfigSchemaChecker() {
// Test a non-existing schema.
$msg = 'Expected SchemaIncompleteException thrown';
try {
\Drupal::config('config_schema_test.noschema')->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');
}
}
......@@ -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'
<?php
/**
* @file
* Contains \Drupal\config_test\SchemaListenerController.
*/
namespace Drupal\config_test;
use Drupal\Core\Config\Schema\SchemaIncompleteException;
use Drupal\Core\Controller\ControllerBase;
/**
* Controller for testing \Drupal\Core\Config\Testing\ConfigSchemaChecker.
*/
class SchemaListenerController extends ControllerBase {
/**
* Tests the WebTestBase tests can use strict schema checking.
*/
public function test() {
try {
$this->config('config_schema_test.noschema')->set('foo', 'bar')->save();
}
catch (SchemaIncompleteException $e) {
return [
'#markup' => $e->getMessage(),
];
}
}
}
......@@ -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);
......
......@@ -202,6 +202,15 @@ abstract class TestBase {
*/
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.
*
......
......@@ -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.
......
......@@ -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');
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment