Issue #3324150 by phenaproxima, Wim Leers, Gábor Hojtsy: Add validation constraints to config_entity.dependencies

merge-requests/3030/head
bnjmnm 2022-12-02 13:01:44 -05:00
parent 22a56817e0
commit adfa35d41f
46 changed files with 1994 additions and 0 deletions

View File

@ -240,6 +240,9 @@ config_dependencies_base:
label: 'Configuration entity dependencies'
sequence:
type: string
constraints:
NotBlank: []
ConfigExists: []
content:
type: sequence
label: 'Content entity dependencies'
@ -250,11 +253,21 @@ config_dependencies_base:
label: 'Module dependencies'
sequence:
type: string
constraints:
NotBlank: []
ExtensionName: []
ExtensionExists: module
theme:
type: sequence
label: 'Theme dependencies'
sequence:
type: string
constraints:
NotBlank: []
ExtensionName: []
ExtensionExists: theme
constraints:
ValidKeys: '<infer>'
config_dependencies:
type: config_dependencies_base
@ -263,6 +276,8 @@ config_dependencies:
enforced:
type: config_dependencies_base
label: 'Enforced configuration dependencies'
constraints:
ValidKeys: '<infer>'
config_entity:
type: mapping

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types = 1);
namespace Drupal\Core\Config\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
/**
* Checks that the value is the name of an existing config object.
*
* @Constraint(
* id = "ConfigExists",
* label = @Translation("Config exists", context = "Validation")
* )
*/
class ConfigExistsConstraint extends Constraint {
/**
* The error message.
*
* @var string
*/
public string $message = "The '@name' config does not exist.";
}

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types = 1);
namespace Drupal\Core\Config\Plugin\Validation\Constraint;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Validates that a given config object exists.
*/
class ConfigExistsConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
/**
* The config factory service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected ConfigFactoryInterface $configFactory;
/**
* Constructs a ConfigExistsConstraintValidator object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
*/
public function __construct(ConfigFactoryInterface $config_factory) {
$this->configFactory = $config_factory;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('config.factory'));
}
/**
* {@inheritdoc}
*/
public function validate(mixed $name, Constraint $constraint) {
if (!in_array($name, $this->configFactory->listAll(), TRUE)) {
$this->context->addViolation($constraint->message, ['@name' => $name]);
}
}
}

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types = 1);
namespace Drupal\Core\Config\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
/**
* Checks that config dependencies contain specific types of entities.
*
* @Constraint(
* id = "RequiredConfigDependencies",
* label = @Translation("Required config dependency types", context = "Validation")
* )
*/
class RequiredConfigDependenciesConstraint extends Constraint {
/**
* The error message.
*
* @var string
*/
public string $message = 'This @entity_type requires a @dependency_type.';
/**
* The IDs of entity types that need to exist in config dependencies.
*
* For example, if an entity requires a filter format in its config
* dependencies, this should contain `filter_format`.
*
* @var string[]
*/
public array $entityTypes = [];
/**
* {@inheritdoc}
*/
public function getRequiredOptions() {
return ['entityTypes'];
}
/**
* {@inheritdoc}
*/
public function getDefaultOption() {
return 'entityTypes';
}
}

View File

@ -0,0 +1,80 @@
<?php
declare(strict_types = 1);
namespace Drupal\Core\Config\Plugin\Validation\Constraint;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\LogicException;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
/**
* Validates the RequiredConfigDependencies constraint.
*/
class RequiredConfigDependenciesConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected EntityTypeManagerInterface $entityTypeManager;
/**
* Constructs a RequiredConfigDependenciesConstraintValidator object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function validate(mixed $entity, Constraint $constraint) {
assert($constraint instanceof RequiredConfigDependenciesConstraint);
// Only config entities can have config dependencies.
if (!$entity instanceof ConfigEntityInterface) {
throw new UnexpectedTypeException($entity, ConfigEntityInterface::class);
}
$config_dependencies = $entity->getDependencies()['config'] ?? [];
foreach ($constraint->entityTypes as $entity_type_id) {
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
if (!$entity_type instanceof ConfigEntityTypeInterface) {
throw new LogicException("'$entity_type_id' is not a config entity type.");
}
// Ensure the current entity type's config prefix is found in the config
// dependencies of the entity being validated.
$pattern = sprintf('/^%s\\.\\w+/', $entity_type->getConfigPrefix());
if (!preg_grep($pattern, $config_dependencies)) {
$this->context->addViolation($constraint->message, [
'@entity_type' => $entity->getEntityType()->getSingularLabel(),
'@dependency_type' => $entity_type->getSingularLabel(),
]);
}
}
}
}

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types = 1);
namespace Drupal\Core\Extension\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
/**
* Checks that the value is the name of an installed extension.
*
* @Constraint(
* id = "ExtensionExists",
* label = @Translation("Extension exists", context = "Validation")
* )
*/
class ExtensionExistsConstraint extends Constraint {
/**
* The error message for a non-existent module.
*
* @var string
*/
public string $moduleMessage = "Module '@name' is not installed.";
/**
* The error message for a non-existent theme.
*
* @var string
*/
public string $themeMessage = "Theme '@name' is not installed.";
/**
* The type of extension to look for. Can be 'module' or 'theme'.
*
* @var string
*/
public string $type;
/**
* {@inheritdoc}
*/
public function getRequiredOptions() {
return ['type'];
}
/**
* {@inheritdoc}
*/
public function getDefaultOption() {
return 'type';
}
}

View File

@ -0,0 +1,80 @@
<?php
declare(strict_types = 1);
namespace Drupal\Core\Extension\Plugin\Validation\Constraint;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Validates that a given extension exists.
*/
class ExtensionExistsConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected ModuleHandlerInterface $moduleHandler;
/**
* The theme handler service.
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface
*/
protected ThemeHandlerInterface $themeHandler;
/**
* Constructs a ExtensionExistsConstraintValidator object.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler service.
*/
public function __construct(ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler) {
$this->moduleHandler = $module_handler;
$this->themeHandler = $theme_handler;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('module_handler'),
$container->get('theme_handler')
);
}
/**
* {@inheritdoc}
*/
public function validate(mixed $extension_name, Constraint $constraint) {
$variables = ['@name' => $extension_name];
switch ($constraint->type) {
case 'module':
if (!$this->moduleHandler->moduleExists($extension_name)) {
$this->context->addViolation($constraint->moduleMessage, $variables);
}
break;
case 'theme':
if (!$this->themeHandler->themeExists($extension_name)) {
$this->context->addViolation($constraint->themeMessage, $variables);
}
break;
default:
throw new \InvalidArgumentException("Unknown extension type: '$constraint->type'");
}
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types = 1);
namespace Drupal\Core\Extension\Plugin\Validation\Constraint;
use Drupal\Core\Extension\ExtensionDiscovery;
use Drupal\Core\Validation\Plugin\Validation\Constraint\RegexConstraint;
/**
* Checks that the value is a valid extension name.
*
* @Constraint(
* id = "ExtensionName",
* label = @Translation("Valid extension name", context = "Validation")
* )
*/
class ExtensionNameConstraint extends RegexConstraint {
/**
* Constructs an ExtensionNameConstraint object.
*
* @param string|array|null $pattern
* The regular expression to test for.
* @param mixed ...$arguments
* Arguments to pass to the parent constructor.
*/
public function __construct(string|array|null $pattern, ...$arguments) {
// Always use the regular expression that ExtensionDiscovery uses to find
// valid extensions.
$pattern = ExtensionDiscovery::PHP_FUNCTION_PATTERN;
parent::__construct($pattern, ...$arguments);
}
}

View File

@ -0,0 +1,97 @@
<?php
declare(strict_types = 1);
namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
use Drupal\Core\Config\Schema\Mapping;
use Drupal\Core\TypedData\MapDataDefinition;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Exception\InvalidArgumentException;
/**
* Checks that all the keys of a mapping are known.
*
* @Constraint(
* id = "ValidKeys",
* label = @Translation("Valid mapping keys", context = "Validation"),
* )
*/
class ValidKeysConstraint extends Constraint {
/**
* The error message if an invalid key appears.
*
* @var string
*/
public string $invalidKeyMessage = "'@key' is not a supported key.";
/**
* The error message if the array being validated is a list.
*
* @var string
*/
public string $indexedArrayMessage = 'Numerically indexed arrays are not allowed.';
/**
* Keys which are allowed in the validated array, or `<infer>` to auto-detect.
*
* @var array|string
*/
public array|string $allowedKeys;
/**
* {@inheritdoc}
*/
public function getDefaultOption() {
return 'allowedKeys';
}
/**
* {@inheritdoc}
*/
public function getRequiredOptions() {
return ['allowedKeys'];
}
/**
* Returns the list of valid keys.
*
* @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context
* The current execution context.
*
* @return string[]
* The keys that will be considered valid.
*/
public function getAllowedKeys(ExecutionContextInterface $context): array {
// If we were given an explicit array of allowed keys, return that.
if (is_array($this->allowedKeys)) {
return $this->allowedKeys;
}
// The only other value we'll accept is the string `<infer>`.
elseif ($this->allowedKeys === '<infer>') {
return static::inferKeys($context->getObject());
}
throw new InvalidArgumentException("'$this->allowedKeys' is not a valid set of allowed keys.");
}
/**
* Tries to auto-detect the schema-defined keys in a mapping.
*
* @param \Drupal\Core\Config\Schema\Mapping $mapping
* The mapping to inspect.
*
* @return string[]
* The keys defined in the mapping's schema.
*/
protected static function inferKeys(Mapping $mapping): array {
$definition = $mapping->getDataDefinition();
assert($definition instanceof MapDataDefinition);
$definition = $definition->toArray();
assert(array_key_exists('mapping', $definition));
return array_keys($definition['mapping']);
}
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types = 1);
namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
/**
* Validates the ValidKeys constraint.
*/
class ValidKeysConstraintValidator extends ConstraintValidator {
/**
* {@inheritdoc}
*/
public function validate(mixed $value, Constraint $constraint) {
assert($constraint instanceof ValidKeysConstraint);
if (!is_array($value)) {
throw new UnexpectedTypeException($value, 'array');
}
// Indexed arrays are invalid by definition. array_is_list() returns TRUE
// for empty arrays, so only do this check if $value is not empty.
if ($value && array_is_list($value)) {
$this->context->addViolation($constraint->indexedArrayMessage);
return;
}
$invalid_keys = array_diff(
array_keys($value),
$constraint->getAllowedKeys($this->context)
);
foreach ($invalid_keys as $key) {
$this->context->addViolation($constraint->invalidKeyMessage, ['@key' => $key]);
}
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace Drupal\Tests\block_content\Kernel;
use Drupal\block_content\Entity\BlockContentType;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
/**
* Tests validation of block_content_type entities.
*
* @group block_content
*/
class BlockContentTypeValidationTest extends ConfigEntityValidationTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['block_content'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entity = BlockContentType::create([
'id' => 'test',
'label' => 'Test',
]);
$this->entity->save();
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Drupal\Tests\comment\Kernel;
use Drupal\comment\Entity\CommentType;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
/**
* Tests validation of comment_type entities.
*
* @group comment
*/
class CommentTypeValidationTest extends ConfigEntityValidationTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['comment', 'node'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entity = CommentType::create([
'id' => 'test',
'label' => 'Test',
'target_entity_type_id' => 'node',
]);
$this->entity->save();
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace Drupal\Tests\contact\Kernel;
use Drupal\contact\Entity\ContactForm;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
/**
* Tests validation of contact_form entities.
*
* @group contact
*/
class ContactFormValidationTest extends ConfigEntityValidationTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['contact', 'user'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entity = ContactForm::create([
'id' => 'test',
'label' => 'Test',
]);
$this->entity->save();
}
}

View File

@ -30,6 +30,11 @@ use Drupal\editor\EditorInterface;
* "editor",
* "settings",
* "image_upload",
* },
* constraints = {
* "RequiredConfigDependencies" = {
* "filter_format"
* }
* }
* )
*/

View File

@ -0,0 +1,65 @@
<?php
namespace Drupal\Tests\editor\Kernel;
use Drupal\editor\Entity\Editor;
use Drupal\filter\Entity\FilterFormat;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
/**
* Tests validation of editor entities.
*
* @group editor
*/
class EditorValidationTest extends ConfigEntityValidationTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['editor', 'editor_test', 'filter'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$format = FilterFormat::create([
'format' => 'test',
'name' => 'Test',
]);
$format->save();
$this->entity = Editor::create([
'format' => $format->id(),
'editor' => 'unicorn',
]);
$this->entity->save();
}
/**
* Tests that validation fails if config dependencies are invalid.
*/
public function testInvalidDependencies(): void {
// Remove the config dependencies from the editor entity.
$dependencies = $this->entity->getDependencies();
$dependencies['config'] = [];
$this->entity->set('dependencies', $dependencies);
$this->assertValidationErrors(['This text editor requires a text format.']);
// Things look sort-of like `filter.format.*` should fail validation
// because they don't exist.
$dependencies['config'] = [
'filter.format',
'filter.format.',
];
$this->entity->set('dependencies', $dependencies);
$this->assertValidationErrors([
'This text editor requires a text format.',
"The 'filter.format' config does not exist.",
"The 'filter.format.' config does not exist.",
]);
}
}

View File

@ -44,6 +44,11 @@ use Drupal\field\FieldConfigInterface;
* "default_value_callback",
* "settings",
* "field_type",
* },
* constraints = {
* "RequiredConfigDependencies" = {
* "field_storage_config"
* }
* }
* )
*/

View File

@ -0,0 +1,56 @@
<?php
namespace Drupal\Tests\field\Kernel\Entity;
use Drupal\field\Entity\FieldConfig;
/**
* Tests validation of field_config entities.
*
* @group field
*/
class FieldConfigValidationTest extends FieldStorageConfigValidationTest {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// The field storage was created in the parent method.
$field_storage = $this->entity;
$this->entity = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'user',
]);
$this->entity->save();
}
/**
* Tests that validation fails if config dependencies are invalid.
*/
public function testInvalidDependencies(): void {
// Remove the config dependencies from the field entity.
$dependencies = $this->entity->getDependencies();
$dependencies['config'] = [];
$this->entity->set('dependencies', $dependencies);
$this->assertValidationErrors(['This field requires a field storage.']);
// Things look sort-of like `field.storage.*.*` should fail validation
// because they don't exist.
$dependencies['config'] = [
'field.storage.fake',
'field.storage.',
'field.storage.user.',
];
$this->entity->set('dependencies', $dependencies);
$this->assertValidationErrors([
"The 'field.storage.fake' config does not exist.",
"The 'field.storage.' config does not exist.",
"The 'field.storage.user.' config does not exist.",
]);
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Drupal\Tests\field\Kernel\Entity;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
/**
* Tests validation of field_storage_config entities.
*
* @group field
*/
class FieldStorageConfigValidationTest extends ConfigEntityValidationTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['field', 'user'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entity = FieldStorageConfig::create([
'type' => 'boolean',
'field_name' => 'test',
'entity_type' => 'user',
]);
$this->entity->save();
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace Drupal\Tests\filter\Kernel;
use Drupal\filter\Entity\FilterFormat;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
/**
* Tests validation of filter_format entities.
*
* @group filter
*/
class FilterFormatValidationTest extends ConfigEntityValidationTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['filter'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entity = FilterFormat::create([
'format' => 'test',
'name' => 'Test',
]);
$this->entity->save();
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace Drupal\Tests\image\Kernel;
use Drupal\image\Entity\ImageStyle;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
/**
* Tests validation of image_style entities.
*
* @group image
*/
class ImageStyleValidationTest extends ConfigEntityValidationTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['image'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entity = ImageStyle::create([
'name' => 'test',
'label' => 'Test',
]);
$this->entity->save();
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Drupal\Tests\language\Kernel;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
/**
* Tests validation of configurable_language entities.
*
* @group language
*/
class ConfigurableLanguageValidationTest extends ConfigEntityValidationTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['language'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entity = ConfigurableLanguage::createFromLangcode('fr');
$this->entity->save();
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace Drupal\Tests\language\Kernel;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
use Drupal\language\Entity\ContentLanguageSettings;
/**
* Tests validation of content_language_settings entities.
*
* @group language
*/
class ContentLanguageSettingsValidationTest extends ConfigEntityValidationTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['language', 'user'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entity = ContentLanguageSettings::create([
'target_entity_type_id' => 'user',
'target_bundle' => 'user',
]);
$this->entity->save();
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace Drupal\Tests\layout_builder\Kernel;
use Drupal\Core\Entity\Entity\EntityViewMode;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
/**
* Tests validation of Layout Builder's entity_view_display entities.
*
* @group layout_builder
*/
class LayoutBuilderEntityViewDisplayValidationTest extends ConfigEntityValidationTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['layout_builder', 'user'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
EntityViewMode::create([
'id' => 'user.layout',
'label' => 'Layout',
'targetEntityType' => 'user',
])->save();
$this->entity = LayoutBuilderEntityViewDisplay::create([
'mode' => 'layout',
'label' => 'Layout',
'targetEntityType' => 'user',
'bundle' => 'user',
]);
$this->entity->save();
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Drupal\Tests\media\Kernel;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
/**
* Tests validation of media_type entities.
*
* @group media
*/
class MediaTypeValidationTest extends ConfigEntityValidationTestBase {
use MediaTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['field', 'media', 'media_test_source'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entity = $this->createMediaType('test');
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Drupal\Tests\node\Kernel;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
/**
* Tests validation of node_type entities.
*
* @group node
*/
class NodeTypeValidationTest extends ConfigEntityValidationTestBase {
use ContentTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['field', 'node', 'text', 'user'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig('node');
$this->entity = $this->createContentType();
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace Drupal\Tests\responsive_image\Kernel;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
use Drupal\responsive_image\Entity\ResponsiveImageStyle;
/**
* Tests validation of responsive_image_style entities.
*
* @group responsive_image
*/
class ResponsiveImageStyleValidationTest extends ConfigEntityValidationTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['breakpoint', 'image', 'responsive_image'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entity = ResponsiveImageStyle::create([
'id' => 'test',
'label' => 'Test',
]);
$this->entity->save();
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace Drupal\Tests\rest\Kernel\Entity;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
use Drupal\rest\Entity\RestResourceConfig;
use Drupal\rest\RestResourceConfigInterface;
/**
* Tests validation of rest_resource_config entities.
*
* @group rest
*/
class RestResourceConfigValidationTest extends ConfigEntityValidationTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['rest', 'serialization'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entity = RestResourceConfig::create([
'id' => 'test',
'plugin_id' => 'entity:date_format',
'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY,
'configuration' => [],
]);
$this->entity->save();
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Drupal\Tests\search\Kernel;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
use Drupal\search\Entity\SearchPage;
/**
* Tests validation of search_page entities.
*
* @group search
*/
class SearchPageValidationTest extends ConfigEntityValidationTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['search', 'user'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entity = SearchPage::create([
'id' => 'test',
'label' => 'Test',
'plugin' => 'user_search',
]);
$this->entity->save();
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace Drupal\Tests\shortcut\Kernel;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
use Drupal\shortcut\Entity\ShortcutSet;
/**
* Tests validation of shortcut_set entities.
*
* @group shortcut
*/
class ShortcutSetValidationTest extends ConfigEntityValidationTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['link', 'shortcut'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig('shortcut');
$this->installEntitySchema('shortcut');
$this->entity = ShortcutSet::create([
'id' => 'test',
'label' => 'Test',
]);
$this->entity->save();
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Drupal\Tests\system\Kernel\Entity;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
use Drupal\system\Entity\Action;
/**
* Tests validation of action entities.
*
* @group system
*/
class ActionValidationTest extends ConfigEntityValidationTestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entity = Action::create([
'id' => 'test',
'label' => 'Test',
'type' => 'test',
'plugin' => 'action_goto_action',
]);
$this->entity->save();
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace Drupal\Tests\system\Kernel\Entity;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
use Drupal\system\Entity\Menu;
/**
* Tests validation of menu entities.
*
* @group system
*/
class MenuValidationTest extends ConfigEntityValidationTestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entity = Menu::create([
'id' => 'test',
'label' => 'Test',
]);
$this->entity->save();
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace Drupal\Tests\taxonomy\Kernel;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
use Drupal\taxonomy\Entity\Vocabulary;
/**
* Tests validation of vocabulary entities.
*
* @group taxonomy
*/
class VocabularyValidationTest extends ConfigEntityValidationTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['taxonomy'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entity = Vocabulary::create([
'vid' => 'test',
'name' => 'Test',
]);
$this->entity->save();
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace Drupal\Tests\user\Kernel;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
use Drupal\user\Entity\Role;
/**
* Tests validation of user_role entities.
*
* @group user
*/
class RoleValidationTest extends ConfigEntityValidationTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['user'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entity = Role::create([
'id' => 'test',
'label' => 'Test',
]);
$this->entity->save();
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace Drupal\Tests\views\Kernel\Entity;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
use Drupal\views\Entity\View;
/**
* Tests validation of view entities.
*
* @group views
*/
class ViewValidationTest extends ConfigEntityValidationTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['views'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entity = View::create([
'id' => 'test',
'label' => 'Test',
]);
$this->entity->save();
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Drupal\Tests\workflows\Kernel;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
use Drupal\workflows\Entity\Workflow;
/**
* Tests validation of workflow entities.
*
* @group workflows
*/
class WorkflowValidationTest extends ConfigEntityValidationTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['workflows', 'workflow_type_test'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entity = Workflow::create([
'id' => 'test',
'label' => 'Test',
'type' => 'workflow_type_test',
]);
$this->entity->save();
}
}

View File

@ -0,0 +1,188 @@
<?php
namespace Drupal\KernelTests\Core\Config;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\KernelTests\KernelTestBase;
/**
* Base class for testing validation of config entities.
*
* @group config
* @group Validation
*/
abstract class ConfigEntityValidationTestBase extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system'];
/**
* The config entity being tested.
*
* @var \Drupal\Core\Config\Entity\ConfigEntityInterface
*/
protected ConfigEntityInterface $entity;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig('system');
// Install Stark so we can add a legitimately installed theme to config
// dependencies.
$this->container->get('theme_installer')->install(['stark']);
$this->container = $this->container->get('kernel')->getContainer();
}
/**
* Data provider for ::testConfigDependenciesValidation().
*
* @return array[]
* The test cases.
*/
public function providerConfigDependenciesValidation(): array {
return [
'valid dependency types' => [
[
'config' => ['system.site'],
'content' => ['node:some-random-uuid'],
'module' => ['system'],
'theme' => ['stark'],
],
[],
],
'unknown dependency type' => [
[
'fun_stuff' => ['star-trek.deep-space-nine'],
],
[
"'fun_stuff' is not a supported key.",
],
],
'empty string in config dependencies' => [
[
'config' => [''],
],
[
'This value should not be blank.',
"The '' config does not exist.",
],
],
'non-existent config dependency' => [
[
'config' => ['fake_settings'],
],
[
"The 'fake_settings' config does not exist.",
],
],
'empty string in module dependencies' => [
[
'module' => [''],
],
[
'This value should not be blank.',
"Module '' is not installed.",
],
],
'invalid module dependency' => [
[
'module' => ['invalid-module-name'],
],
[
'This value is not valid.',
"Module 'invalid-module-name' is not installed.",
],
],
'non-installed module dependency' => [
[
'module' => ['bad_judgment'],
],
[
"Module 'bad_judgment' is not installed.",
],
],
'empty string in theme dependencies' => [
[
'theme' => [''],
],
[
'This value should not be blank.',
"Theme '' is not installed.",
],
],
'invalid theme dependency' => [
[
'theme' => ['invalid-theme-name'],
],
[
'This value is not valid.',
"Theme 'invalid-theme-name' is not installed.",
],
],
'non-installed theme dependency' => [
[
'theme' => ['ugly_theme'],
],
[
"Theme 'ugly_theme' is not installed.",
],
],
];
}
/**
* Tests validation of config dependencies.
*
* @param array[] $dependencies
* The dependencies that should be added to the config entity under test.
* @param string[] $expected_messages
* The expected constraint violation messages.
*
* @dataProvider providerConfigDependenciesValidation
*/
public function testConfigDependenciesValidation(array $dependencies, array $expected_messages): void {
$this->assertInstanceOf(ConfigEntityInterface::class, $this->entity);
// The entity should have valid data to begin with.
$this->assertValidationErrors([]);
// Add the dependencies we were given to the dependencies that may already
// exist in the entity.
$dependencies = NestedArray::mergeDeep($this->entity->getDependencies(), $dependencies);
$this->entity->set('dependencies', $dependencies);
$this->assertValidationErrors($expected_messages);
// Enforce these dependencies, and ensure we get the same results.
$this->entity->set('dependencies', [
'enforced' => $dependencies,
]);
$this->assertValidationErrors($expected_messages);
}
/**
* Asserts a set of validation errors is raised when the entity is validated.
*
* @param string[] $expected_messages
* The expected validation error messages.
*/
protected function assertValidationErrors(array $expected_messages): void {
/** @var \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data */
$typed_data = $this->container->get('typed_data_manager');
$definition = $typed_data->createDataDefinition('entity:' . $this->entity->getEntityTypeId());
$violations = $typed_data->create($definition, $this->entity)->validate();
$actual_messages = [];
foreach ($violations as $violation) {
$actual_messages[] = (string) $violation->getMessage();
}
$this->assertSame($expected_messages, $actual_messages);
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace Drupal\KernelTests\Core\Config;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the ConfigExists constraint validator.
*
* @group config
* @group Validation
*
* @covers \Drupal\Core\Config\Plugin\Validation\Constraint\ConfigExistsConstraint
* @covers \Drupal\Core\Config\Plugin\Validation\Constraint\ConfigExistsConstraintValidator
*/
class ConfigExistsConstraintValidatorTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system'];
/**
* Tests the ConfigExists constraint validator.
*/
public function testValidation(): void {
// Create a data definition that specifies the value must be a string with
// the name of an existing piece of config.
$definition = DataDefinition::create('string')
->addConstraint('ConfigExists');
/** @var \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data */
$typed_data = $this->container->get('typed_data_manager');
$data = $typed_data->create($definition, 'system.site');
$violations = $data->validate();
$this->assertCount(1, $violations);
$this->assertSame("The 'system.site' config does not exist.", (string) $violations->get(0)->getMessage());
$this->installConfig('system');
$this->assertCount(0, $data->validate());
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Drupal\KernelTests\Core\Entity;
use Drupal\Core\Field\Entity\BaseFieldOverride;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
/**
* Tests validation of base_field_override entities.
*
* @group Entity
* @group Validation
*/
class BaseFieldOverrideValidationTest extends ConfigEntityValidationTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['user'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$fields = $this->container->get('entity_field.manager')
->getBaseFieldDefinitions('user');
$this->entity = BaseFieldOverride::createFromBaseFieldDefinition(reset($fields), 'user');
$this->entity->save();
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Drupal\KernelTests\Core\Entity;
use Drupal\Core\Datetime\Entity\DateFormat;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
/**
* Tests validation of date_format entities.
*
* @group Entity
* @group Validation
*/
class DateFormatValidationTest extends ConfigEntityValidationTestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entity = DateFormat::create([
'id' => 'test',
'label' => 'Test',
'pattern' => 'Y-m-d',
]);
$this->entity->save();
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Drupal\KernelTests\Core\Entity;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
/**
* Tests validation of entity_form_display entities.
*
* @group Entity
* @group Validation
*/
class EntityFormDisplayValidationTest extends EntityFormModeValidationTest {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entity = EntityFormDisplay::create([
'label' => 'Test',
'targetEntityType' => 'user',
'bundle' => 'user',
// The mode was created by the parent class.
'mode' => 'test',
]);
$this->entity->save();
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Drupal\KernelTests\Core\Entity;
use Drupal\Core\Entity\Entity\EntityFormMode;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
/**
* Tests validation of entity_form_mode entities.
*
* @group Entity
* @group Validation
*/
class EntityFormModeValidationTest extends ConfigEntityValidationTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['user'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig('user');
$this->entity = EntityFormMode::create([
'id' => 'user.test',
'label' => 'Test',
'targetEntityType' => 'user',
]);
$this->entity->save();
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Drupal\KernelTests\Core\Entity;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
/**
* Tests validation of entity_view_display entities.
*
* @group Entity
* @group Validation
*/
class EntityViewDisplayValidationTest extends EntityViewModeValidationTest {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entity = EntityViewDisplay::create([
'label' => 'Test',
'targetEntityType' => 'user',
'bundle' => 'user',
// The mode was created by the parent class.
'mode' => 'test',
]);
$this->entity->save();
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Drupal\KernelTests\Core\Entity;
use Drupal\Core\Entity\Entity\EntityViewMode;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
/**
* Tests validation of entity_view_mode entities.
*
* @group Entity
* @group Validation
*/
class EntityViewModeValidationTest extends ConfigEntityValidationTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['user'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig('user');
$this->entity = EntityViewMode::create([
'id' => 'user.test',
'label' => 'Test',
'targetEntityType' => 'user',
]);
$this->entity->save();
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace Drupal\KernelTests\Core\Extension;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the ExtensionExists constraint validator.
*
* @group Validation
*
* @covers \Drupal\Core\Extension\Plugin\Validation\Constraint\ExtensionExistsConstraint
* @covers \Drupal\Core\Extension\Plugin\Validation\Constraint\ExtensionExistsConstraintValidator
*/
class ExtensionExistsConstraintValidatorTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system'];
/**
* Tests the ExtensionExists constraint validator.
*/
public function testValidation(): void {
// Create a data definition that specifies the value must be a string with
// the name of an installed module.
$definition = DataDefinition::create('string')
->addConstraint('ExtensionExists', 'module');
/** @var \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data */
$typed_data = $this->container->get('typed_data_manager');
$data = $typed_data->create($definition, 'user');
$violations = $data->validate();
$this->assertCount(1, $violations);
$this->assertSame("Module 'user' is not installed.", (string) $violations->get(0)->getMessage());
$this->enableModules(['user']);
$this->assertCount(0, $data->validate());
$definition->setConstraints(['ExtensionExists' => 'theme']);
$data = $typed_data->create($definition, 'stark');
$violations = $data->validate();
$this->assertCount(1, $violations);
$this->assertSame("Theme 'stark' is not installed.", (string) $violations->get(0)->getMessage());
$this->assertTrue($this->container->get('theme_installer')->install(['stark']));
// Installing the theme rebuilds the container, so we need to ensure the
// constraint is instantiated with an up-to-date theme handler.
$data = $this->container->get('kernel')
->getContainer()
->get('typed_data_manager')
->create($definition, 'stark');
$this->assertCount(0, $data->validate());
// Anything but a module or theme should raise an exception.
$definition->setConstraints(['ExtensionExists' => 'profile']);
$this->expectExceptionMessage("Unknown extension type: 'profile'");
$data->validate();
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace Drupal\KernelTests\Core\Extension;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the ExtensionName constraint.
*
* @group Validation
*
* @covers \Drupal\Core\Extension\Plugin\Validation\Constraint\ExtensionNameConstraint
*/
class ExtensionNameConstraintTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system'];
/**
* Tests the ExtensionName constraint.
*/
public function testValidation(): void {
// Create a data definition that specifies the value must be a string with
// the name of a valid extension.
$definition = DataDefinition::create('string')
->addConstraint('ExtensionName');
/** @var \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data */
$typed_data = $this->container->get('typed_data_manager');
$data = $typed_data->create($definition, 'user');
$this->assertCount(0, $data->validate());
$data->setValue('invalid-name');
$violations = $data->validate();
$this->assertCount(1, $violations);
$this->assertSame('This value is not valid.', (string) $violations->get(0)->getMessage());
}
}

View File

@ -0,0 +1,97 @@
<?php
namespace Drupal\KernelTests\Core\TypedData;
use Drupal\Core\TypedData\DataDefinition;
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 {
/**
* Tests the ValidKeys constraint validator.
*/
public function testValidation(): void {
// Create a data definition that specifies certain allowed keys.
$definition = DataDefinition::create('any')
->addConstraint('ValidKeys', ['north', 'south', 'west']);
/** @var \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data */
$typed_data = $this->container->get('typed_data_manager');
// Passing a non-array value should raise an exception.
try {
$typed_data->create($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_data->create($definition, [])->validate());
// Indexed arrays are never valid.
$violations = $typed_data->create($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_data->create($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_data->create($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_data->create($definition, $value)->validate();
$this->assertCount(0, $violations);
}
/**
* 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();
}
}