ValidKeysConstraintValidatorTest.php
Same filename in other branches
Namespace
Drupal\KernelTests\Core\TypedDataFile
-
core/
tests/ Drupal/ KernelTests/ Core/ TypedData/ ValidKeysConstraintValidatorTest.php
View source
<?php
declare (strict_types=1);
namespace Drupal\KernelTests\Core\TypedData;
use Drupal\block\Entity\Block;
use Drupal\Core\TypedData\MapDataDefinition;
use Drupal\Core\TypedData\TraversableTypedDataInterface;
use Drupal\KernelTests\KernelTestBase;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
/**
* Tests the ValidKeys validation constraint.
*
* @group Validation
*
* @covers \Drupal\Core\Validation\Plugin\Validation\Constraint\ValidKeysConstraint
* @covers \Drupal\Core\Validation\Plugin\Validation\Constraint\ValidKeysConstraintValidator
*/
class ValidKeysConstraintValidatorTest extends KernelTestBase {
/**
* The typed config under test.
*
* @var \Drupal\Core\TypedData\TraversableTypedDataInterface
*
* @see \Drupal\Core\Config\TypedConfigManagerInterface::get()
*/
protected TraversableTypedDataInterface $config;
/**
* {@inheritdoc}
*/
protected function setUp() : void {
parent::setUp();
// Install the Block module and create a Block config entity, so that we can
// test that the validator infers the required keys from a defined schema.
$this->enableModules([
'system',
'block',
]);
// Also install the config_schema_test module, to enable testing with
// config entities as the example in the test cases below, simulating both
// possible schema states: fully validatable and not fully validatable.
// @see \Drupal\KernelTests\Config\Schema\MappingTest::testMappingInterpretations()
$this->enableModules([
'config_schema_test',
]);
/** @var \Drupal\Core\Extension\ThemeInstallerInterface $theme_installer */
$theme_installer = $this->container
->get('theme_installer');
$theme_installer->install([
'stark',
]);
$block = Block::create([
'id' => 'branding',
'plugin' => 'system_branding_block',
'theme' => 'stark',
'status' => TRUE,
'weight' => 0,
'provider' => 'system',
'settings' => [
'use_site_logo' => TRUE,
'use_site_name' => TRUE,
'use_site_slogan' => TRUE,
'label_display' => FALSE,
// TRICKY: these 4 are inherited from `type: block_settings`.
'status' => TRUE,
'info' => '',
'view_mode' => 'full',
'context_mapping' => [],
],
]);
$block->save();
$this->config = $this->container
->get('config.typed')
->get('block.block.branding');
}
/**
* Tests detecting unsupported keys.
*
* @see \Drupal\Core\Validation\Plugin\Validation\Constraint\ValidKeysConstraint::$invalidKeyMessage
*/
public function testSupportedKeys() : void {
// Start from the valid config.
$this->assertEmpty($this->config
->validate());
// Then modify only one thing: generate a non-existent `foobar` setting.
$data = $this->config
->toArray();
$data['settings']['foobar'] = TRUE;
$this->assertValidationErrors('block.block.branding', $data, [
'settings.foobar' => "'foobar' is not a supported key.",
]);
}
/**
* Tests detecting unknown keys.
*
* @see \Drupal\Core\Validation\Plugin\Validation\Constraint\ValidKeysConstraint::$dynamicInvalidKeyMessage
*/
public function testUnknownKeys() : void {
// Start from the valid config.
$this->assertEmpty($this->config
->validate());
// Then modify only one thing: the block plugin that is being used.
$data = $this->config
->toArray();
$data['plugin'] = 'system_powered_by_block';
$this->assertValidationErrors('block.block.branding', $data, [
'settings' => [
"'use_site_logo' is an unknown key because plugin is system_powered_by_block (see config schema type block.settings.*).",
"'use_site_name' is an unknown key because plugin is system_powered_by_block (see config schema type block.settings.*).",
"'use_site_slogan' is an unknown key because plugin is system_powered_by_block (see config schema type block.settings.*).",
],
]);
}
/**
* Tests detecting missing required keys.
*
* @testWith [true, {"settings": "'label_display' is a required key."}]
* [false, {}]
*
* @see \Drupal\Core\Validation\Plugin\Validation\Constraint\ValidKeysConstraint::$missingRequiredKeyMessage
*/
public function testRequiredKeys(bool $block_is_fully_validatable, array $expected_validation_errors) : void {
// Set or unset the `FullyValidatable` constraint on `block.block.*`.
\Drupal::state()->set('config_schema_test_block_fully_validatable', $block_is_fully_validatable);
$this->container
->get('kernel')
->rebuildContainer();
$this->config = $this->container
->get('config.typed')
->get('block.block.branding');
// Start from the valid config.
$this->assertEmpty($this->config
->validate());
// Then modify only one thing: remove the `label_display` setting.
$data = $this->config
->toArray();
unset($data['settings']['label_display']);
$this->config = $this->container
->get('config.typed')
->createFromNameAndData('block.block.branding', $data);
// Now 1 validation error should be triggered: one for the missing
// (statically) required key. It is only required because all block plugins
// are required to set it: see `type: block_settings`.
// @see \Drupal\system\Plugin\Block\SystemBrandingBlock::defaultConfiguration()
// @see \Drupal\system\Plugin\Block\SystemPoweredByBlock::defaultConfiguration()
$this->assertValidationErrors('block.block.branding', $data, $expected_validation_errors);
}
/**
* Tests detecting missing dynamically required keys.
*
* @testWith [true, {"settings": "'use_site_name' is a required key because plugin is system_branding_block (see config schema type block.settings.system_branding_block)."}]
* [false, {}]
*
* @see \Drupal\Core\Validation\Plugin\Validation\Constraint\ValidKeysConstraint::$dynamicMissingRequiredKeyMessage
*/
public function testDynamicallyRequiredKeys(bool $block_is_fully_validatable, array $expected_validation_errors) : void {
// Set or unset the `FullyValidatable` constraint on `block.block.*`.
\Drupal::state()->set('config_schema_test_block_fully_validatable', $block_is_fully_validatable);
$this->container
->get('kernel')
->rebuildContainer();
$this->config = $this->container
->get('config.typed')
->get('block.block.branding');
// Start from the valid config.
$this->assertEmpty($this->config
->validate());
// Then modify only one thing: remove the `use_site_name` setting.
$data = $this->config
->toArray();
unset($data['settings']['use_site_name']);
$this->config = $this->container
->get('config.typed')
->createFromNameAndData('block.block.branding', $data);
// Now 1 validation error should be triggered: one for the missing
// required key. It is only dynamically required because not
// all block plugins support this key in their configuration.
// @see \Drupal\system\Plugin\Block\SystemBrandingBlock::defaultConfiguration()
// @see \Drupal\system\Plugin\Block\SystemPoweredByBlock::defaultConfiguration()
$this->assertValidationErrors('block.block.branding', $data, $expected_validation_errors);
}
/**
* Tests detecting both unknown and required keys.
*
* @testWith [true, ["'primary' is a required key because plugin is local_tasks_block (see config schema type block.settings.local_tasks_block).", "'secondary' is a required key because plugin is local_tasks_block (see config schema type block.settings.local_tasks_block)."]]
* [false, []]
*
* @see \Drupal\Core\Validation\Plugin\Validation\Constraint\ValidKeysConstraint::$dynamicInvalidKeyMessage
* @see \Drupal\Core\Validation\Plugin\Validation\Constraint\ValidKeysConstraint::$dynamicMissingRequiredKeyMessage
*/
public function testBothUnknownAndDynamicallyRequiredKeys(bool $block_is_fully_validatable, array $additional_expected_validation_errors) : void {
// Set or unset the `FullyValidatable` constraint on `block.block.*`.
\Drupal::state()->set('config_schema_test_block_fully_validatable', $block_is_fully_validatable);
$this->container
->get('kernel')
->rebuildContainer();
$this->config = $this->container
->get('config.typed')
->get('block.block.branding');
// Start from the valid config.
$this->assertEmpty($this->config
->validate());
// Then modify only one thing: the block plugin that is being used.
$data = $this->config
->toArray();
$data['plugin'] = 'local_tasks_block';
$this->config = $this->container
->get('config.typed')
->createFromNameAndData('block.block.branding', $data);
// Now 3 validation errors should be triggered: one for each of the settings
// that exist in the "branding" block but not the "powered by" block.
// @see \Drupal\system\Plugin\Block\SystemBrandingBlock::defaultConfiguration()
// @see \Drupal\system\Plugin\Block\SystemPoweredByBlock::defaultConfiguration()
$this->assertValidationErrors('block.block.branding', $data, [
'settings' => [
"'use_site_logo' is an unknown key because plugin is local_tasks_block (see config schema type block.settings.local_tasks_block).",
"'use_site_name' is an unknown key because plugin is local_tasks_block (see config schema type block.settings.local_tasks_block).",
"'use_site_slogan' is an unknown key because plugin is local_tasks_block (see config schema type block.settings.local_tasks_block).",
$additional_expected_validation_errors,
],
]);
}
/**
* Tests the ValidKeys constraint validator.
*/
public function testValidation() : void {
// Create a data definition that specifies certain allowed keys.
$definition = MapDataDefinition::create('mapping')->addConstraint('ValidKeys', [
'north',
'south',
'west',
]);
$definition['mapping'] = [
'north' => [
'type' => 'string',
'requiredKey' => FALSE,
],
'east' => [
'type' => 'string',
'requiredKey' => FALSE,
],
'south' => [
'type' => 'string',
'requiredKey' => FALSE,
],
'west' => [
'type' => 'string',
'requiredKey' => FALSE,
],
];
// @todo Remove this line in https://www.drupal.org/project/drupal/issues/3403782
$definition->setClass('Drupal\\Core\\Config\\Schema\\Mapping');
/** @var \Drupal\Core\TypedData\TypedDataManagerInterface $typed_config */
$typed_config = $this->container
->get('config.typed');
// @see \Drupal\Core\Config\TypedConfigManager::buildDataDefinition()
// @see \Drupal\Core\TypedData\TypedDataManager::createDataDefinition()
$definition->setTypedDataManager($typed_config);
// Passing a non-array value should raise an exception.
try {
// TRICKY: we must clone the definition because the instance is modified
// when processing.
// @see \Drupal\Core\Config\Schema\Mapping::processRequiredKeyFlags()
$typed_config->create(clone $definition, 2501)
->validate();
$this->fail('Expected an exception but none was raised.');
} catch (UnexpectedTypeException $e) {
$this->assertSame('Expected argument of type "array", "int" given', $e->getMessage());
}
// Empty arrays are valid.
$this->assertCount(0, $typed_config->create(clone $definition, [])
->validate());
// Indexed arrays are never valid.
$violations = $typed_config->create(clone $definition, [
'north',
'south',
])
->validate();
$this->assertCount(1, $violations);
$this->assertSame('Numerically indexed arrays are not allowed.', (string) $violations->get(0)
->getMessage());
// Arrays with automatically assigned keys, AND a valid key, should be
// considered invalid overall.
$violations = $typed_config->create(clone $definition, [
'north',
'south' => 'west',
])
->validate();
$this->assertCount(1, $violations);
$this->assertSame("'0' is not a supported key.", (string) $violations->get(0)
->getMessage());
// Associative arrays with an invalid key should be invalid.
$violations = $typed_config->create(clone $definition, [
'north' => 'south',
'east' => 'west',
])
->validate();
$this->assertCount(1, $violations);
$this->assertSame("'east' is not a supported key.", (string) $violations->get(0)
->getMessage());
// If the array only contains the allowed keys, it's fine.
$value = [
'north' => 'Boston',
'south' => 'Atlanta',
'west' => 'San Francisco',
];
$violations = $typed_config->create(clone $definition, $value)
->validate();
$this->assertCount(0, $violations);
// If, in the mapping definition, some keys do NOT have
// `requiredKey: false` set, then they MUST be set. In other
// words, all keys are required unless they individually
// specify otherwise.
// First test without changing the value: no error should occur because all
// keys passed to the ValidKeys constraint have a value.
unset($definition['mapping']['south']['requiredKey']);
unset($definition['mapping']['east']['requiredKey']);
$violations = $typed_config->create(clone $definition, $value)
->validate();
$this->assertCount(0, $violations);
// If in the mapping definition some keys that do NOT have
// `requiredKey: false` set, then they MUST be set.
// First test without changing the value: no error should occur because all
// keys passed to the ValidKeys constraint have a value.
unset($definition['mapping']['south']['requiredKey']);
unset($definition['mapping']['east']['requiredKey']);
$violations = $typed_config->create(clone $definition, $value)
->validate();
$this->assertCount(0, $violations);
// Then remove the required key-value pair: this must trigger an error, but
// only if the root type has opted in.
unset($value['south']);
$violations = $typed_config->create(clone $definition, $value)
->validate();
$this->assertCount(0, $violations);
$definition->addConstraint('FullyValidatable', NULL);
$violations = $typed_config->create(clone $definition, $value)
->validate();
$this->assertCount(1, $violations);
$this->assertSame("'south' is a required key.", (string) $violations->get(0)
->getMessage());
}
/**
* Tests that valid keys can be inferred from the data definition.
*/
public function testValidKeyInference() : void {
// Install the System module and its config so that we can test that the
// validator infers the allowed keys from a defined schema.
$this->enableModules([
'system',
]);
$this->installConfig('system');
$config = $this->container
->get('config.typed')
->get('system.site');
$config->getDataDefinition()
->addConstraint('ValidKeys', '<infer>');
$data = $config->getValue();
$data['invalid-key'] = "There's a snake in my boots.";
$config->setValue($data);
$violations = $config->validate();
$this->assertCount(1, $violations);
$this->assertSame("'invalid-key' is not a supported key.", (string) $violations->get(0)
->getMessage());
// Ensure that ValidKeys will freak out if the option is not exactly
// `<infer>`.
$config->getDataDefinition()
->addConstraint('ValidKeys', 'infer');
$this->expectExceptionMessage("'infer' is not a valid set of allowed keys.");
$config->validate();
}
/**
* Tests ValidKeys constraint validator detecting optional keys.
*/
public function testMarkedAsOptional() : void {
\Drupal::state()->set('config_schema_test_block_fully_validatable', TRUE);
$this->container
->get('kernel')
->rebuildContainer();
$this->config = $this->container
->get('config.typed')
->get('block.block.branding');
$violations = $this->config
->validate();
$this->assertCount(0, $violations);
// Reference to the mapping in the schema, to allow adjusting it for testing
// purposes.
assert($this->config
->getDataDefinition() instanceof MapDataDefinition);
$mapping = $this->config
->getDataDefinition()['mapping'];
// Removing a key-value pair should trigger a validation error.
$data = $this->config
->getValue();
unset($data['status']);
$this->config
->setValue($data);
$violations = $this->config
->validate();
$this->assertCount(1, $violations);
$this->assertSame("'status' is a required key.", (string) $violations->get(0)
->getMessage());
// Unless a key is explicitly marked as optional.
$mapping['status']['requiredKey'] = FALSE;
$this->config
->getDataDefinition()['mapping'] = $mapping;
$violations = $this->config
->validate();
$this->assertCount(0, $violations);
}
/**
* Asserts a set of validation errors is raised when the config is validated.
*
* @param string $config_name
* The machine name of the configuration.
* @param array $config_data
* The data associated with the configuration. Note: This configuration
* doesn't yet have to be stored.
* @param array<string, string|string[]> $expected_messages
* The expected validation error messages. Keys are property paths, values
* are the expected messages: a string if a single message is expected, an
* array of strings if multiple are expected.
*/
protected function assertValidationErrors(string $config_name, array $config_data, array $expected_messages) : void {
$violations = $this->container
->get('config.typed')
->createFromNameAndData($config_name, $config_data)
->validate();
$actual_messages = [];
foreach ($violations as $violation) {
$property_path = $violation->getPropertyPath();
if (!isset($actual_messages[$property_path])) {
$actual_messages[$property_path] = (string) $violation->getMessage();
}
else {
// Transform value from string to array.
if (is_string($actual_messages[$property_path])) {
$actual_messages[$property_path] = (array) $actual_messages[$violation->getPropertyPath()];
}
// And append.
$actual_messages[$property_path][] = (string) $violation->getMessage();
}
}
ksort($expected_messages);
ksort($actual_messages);
$this->assertSame($expected_messages, $actual_messages);
}
}
Classes
Title | Deprecated | Summary |
---|---|---|
ValidKeysConstraintValidatorTest | Tests the ValidKeys validation constraint. |
Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.