diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml index e6d7f243ac10ff9073eee2753421bfddb6c22064..4476e98d0c7efaecc72b038c75021f509f1947ba 100644 --- a/core/config/schema/core.data_types.schema.yml +++ b/core/config/schema/core.data_types.schema.yml @@ -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 diff --git a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ConfigExistsConstraint.php b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ConfigExistsConstraint.php new file mode 100644 index 0000000000000000000000000000000000000000..006cebd464d4ae084f23b0a7a3837902164f3a71 --- /dev/null +++ b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ConfigExistsConstraint.php @@ -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."; + +} diff --git a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ConfigExistsConstraintValidator.php b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ConfigExistsConstraintValidator.php new file mode 100644 index 0000000000000000000000000000000000000000..590a3056a110eb7b56674ba1def000eae3d97826 --- /dev/null +++ b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ConfigExistsConstraintValidator.php @@ -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]); + } + } + +} diff --git a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/RequiredConfigDependenciesConstraint.php b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/RequiredConfigDependenciesConstraint.php new file mode 100644 index 0000000000000000000000000000000000000000..39bb4e18417561915b56e90bd1999fec490579c9 --- /dev/null +++ b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/RequiredConfigDependenciesConstraint.php @@ -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'; + } + +} diff --git a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/RequiredConfigDependenciesConstraintValidator.php b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/RequiredConfigDependenciesConstraintValidator.php new file mode 100644 index 0000000000000000000000000000000000000000..17802873cae67e69e98f7e8b5bfd7029a19d5887 --- /dev/null +++ b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/RequiredConfigDependenciesConstraintValidator.php @@ -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(), + ]); + } + } + } + +} diff --git a/core/lib/Drupal/Core/Extension/Plugin/Validation/Constraint/ExtensionExistsConstraint.php b/core/lib/Drupal/Core/Extension/Plugin/Validation/Constraint/ExtensionExistsConstraint.php new file mode 100644 index 0000000000000000000000000000000000000000..3432531564e94995d33fee7aab1938e0651812ad --- /dev/null +++ b/core/lib/Drupal/Core/Extension/Plugin/Validation/Constraint/ExtensionExistsConstraint.php @@ -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'; + } + +} diff --git a/core/lib/Drupal/Core/Extension/Plugin/Validation/Constraint/ExtensionExistsConstraintValidator.php b/core/lib/Drupal/Core/Extension/Plugin/Validation/Constraint/ExtensionExistsConstraintValidator.php new file mode 100644 index 0000000000000000000000000000000000000000..404d5dcf38c220add9550b1e895daee2e1515856 --- /dev/null +++ b/core/lib/Drupal/Core/Extension/Plugin/Validation/Constraint/ExtensionExistsConstraintValidator.php @@ -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'"); + } + } + +} diff --git a/core/lib/Drupal/Core/Extension/Plugin/Validation/Constraint/ExtensionNameConstraint.php b/core/lib/Drupal/Core/Extension/Plugin/Validation/Constraint/ExtensionNameConstraint.php new file mode 100644 index 0000000000000000000000000000000000000000..5721ba0bfd2c3076735e218ea2473333ca69a57c --- /dev/null +++ b/core/lib/Drupal/Core/Extension/Plugin/Validation/Constraint/ExtensionNameConstraint.php @@ -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); + } + +} diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ValidKeysConstraint.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ValidKeysConstraint.php new file mode 100644 index 0000000000000000000000000000000000000000..842615c9ea2412fcc7685a568addc3a91b0cfb2d --- /dev/null +++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ValidKeysConstraint.php @@ -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']); + } + +} diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ValidKeysConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ValidKeysConstraintValidator.php new file mode 100644 index 0000000000000000000000000000000000000000..63562f5e80e91bac7abcdf77c343f226d791cadc --- /dev/null +++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ValidKeysConstraintValidator.php @@ -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]); + } + } + +} diff --git a/core/modules/block_content/tests/src/Kernel/BlockContentTypeValidationTest.php b/core/modules/block_content/tests/src/Kernel/BlockContentTypeValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..dd3683fd20c4beb8150c6252715c798dded4bc5d --- /dev/null +++ b/core/modules/block_content/tests/src/Kernel/BlockContentTypeValidationTest.php @@ -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(); + } + +} diff --git a/core/modules/comment/tests/src/Kernel/CommentTypeValidationTest.php b/core/modules/comment/tests/src/Kernel/CommentTypeValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..cc1891879c3efd9ce30550d286b5b43889a78c01 --- /dev/null +++ b/core/modules/comment/tests/src/Kernel/CommentTypeValidationTest.php @@ -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(); + } + +} diff --git a/core/modules/contact/tests/src/Kernel/ContactFormValidationTest.php b/core/modules/contact/tests/src/Kernel/ContactFormValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..b84b2ff626be18dd5f7123e8005dd27744d4b36f --- /dev/null +++ b/core/modules/contact/tests/src/Kernel/ContactFormValidationTest.php @@ -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(); + } + +} diff --git a/core/modules/editor/src/Entity/Editor.php b/core/modules/editor/src/Entity/Editor.php index 8b2d4fcea79680179ba2918dbab2e06f3abc06da..dd102d9179750dbfdd96ab574f9a7eeb86c602c1 100644 --- a/core/modules/editor/src/Entity/Editor.php +++ b/core/modules/editor/src/Entity/Editor.php @@ -30,6 +30,11 @@ * "editor", * "settings", * "image_upload", + * }, + * constraints = { + * "RequiredConfigDependencies" = { + * "filter_format" + * } * } * ) */ diff --git a/core/modules/editor/tests/src/Kernel/EditorValidationTest.php b/core/modules/editor/tests/src/Kernel/EditorValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..696060ffbf6a99f2ac99347d97217f181c4e2cb6 --- /dev/null +++ b/core/modules/editor/tests/src/Kernel/EditorValidationTest.php @@ -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.", + ]); + } + +} diff --git a/core/modules/field/src/Entity/FieldConfig.php b/core/modules/field/src/Entity/FieldConfig.php index 8067f5be5df062d33f243b893bfe4a96c49b5078..66d88deb2d3b7ba753a6c7007ec022160b7b8ee4 100644 --- a/core/modules/field/src/Entity/FieldConfig.php +++ b/core/modules/field/src/Entity/FieldConfig.php @@ -44,6 +44,11 @@ * "default_value_callback", * "settings", * "field_type", + * }, + * constraints = { + * "RequiredConfigDependencies" = { + * "field_storage_config" + * } * } * ) */ diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..ee5cc5243757c496fe84ee98cc3b546797a535c8 --- /dev/null +++ b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php @@ -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.", + ]); + } + +} diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..95ba5ec42bd8d4ec323bcaf865daa3afb6238f72 --- /dev/null +++ b/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php @@ -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(); + } + +} diff --git a/core/modules/filter/tests/src/Kernel/FilterFormatValidationTest.php b/core/modules/filter/tests/src/Kernel/FilterFormatValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..0c8fb27ec440957906437dbcecbf63b932a07f4a --- /dev/null +++ b/core/modules/filter/tests/src/Kernel/FilterFormatValidationTest.php @@ -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(); + } + +} diff --git a/core/modules/image/tests/src/Kernel/ImageStyleValidationTest.php b/core/modules/image/tests/src/Kernel/ImageStyleValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..65e9288c449f1d87e9548d72b009b417639657a6 --- /dev/null +++ b/core/modules/image/tests/src/Kernel/ImageStyleValidationTest.php @@ -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(); + } + +} diff --git a/core/modules/language/tests/src/Kernel/ConfigurableLanguageValidationTest.php b/core/modules/language/tests/src/Kernel/ConfigurableLanguageValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..7c95b3163cd07b5927de03f0d0bf5a4e4f53e39e --- /dev/null +++ b/core/modules/language/tests/src/Kernel/ConfigurableLanguageValidationTest.php @@ -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(); + } + +} diff --git a/core/modules/language/tests/src/Kernel/ContentLanguageSettingsValidationTest.php b/core/modules/language/tests/src/Kernel/ContentLanguageSettingsValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..01a2e6a77ed9782ff00016327b0579a673357125 --- /dev/null +++ b/core/modules/language/tests/src/Kernel/ContentLanguageSettingsValidationTest.php @@ -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(); + } + +} diff --git a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayValidationTest.php b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..0c6032afb6a5550b16827326e0d86ac82d8ed6f0 --- /dev/null +++ b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayValidationTest.php @@ -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(); + } + +} diff --git a/core/modules/media/tests/src/Kernel/MediaTypeValidationTest.php b/core/modules/media/tests/src/Kernel/MediaTypeValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..c5f2774b2c896a7473a8ce570b0cbcd43491c100 --- /dev/null +++ b/core/modules/media/tests/src/Kernel/MediaTypeValidationTest.php @@ -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'); + } + +} diff --git a/core/modules/node/tests/src/Kernel/NodeTypeValidationTest.php b/core/modules/node/tests/src/Kernel/NodeTypeValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..c5bcc47355e640dacae2516e98c20d8c582da1ff --- /dev/null +++ b/core/modules/node/tests/src/Kernel/NodeTypeValidationTest.php @@ -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(); + } + +} diff --git a/core/modules/responsive_image/tests/src/Kernel/ResponsiveImageStyleValidationTest.php b/core/modules/responsive_image/tests/src/Kernel/ResponsiveImageStyleValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..1c9a4cb98c92329f86eb6ee2b4fe7a137ff3fe49 --- /dev/null +++ b/core/modules/responsive_image/tests/src/Kernel/ResponsiveImageStyleValidationTest.php @@ -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(); + } + +} diff --git a/core/modules/rest/tests/src/Kernel/Entity/RestResourceConfigValidationTest.php b/core/modules/rest/tests/src/Kernel/Entity/RestResourceConfigValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..0d8cb96272cbc572abfd55b3b39b555dbc94ffe7 --- /dev/null +++ b/core/modules/rest/tests/src/Kernel/Entity/RestResourceConfigValidationTest.php @@ -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(); + } + +} diff --git a/core/modules/search/tests/src/Kernel/SearchPageValidationTest.php b/core/modules/search/tests/src/Kernel/SearchPageValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..eb35264fd3f1b373aa8c43501f75726d92521187 --- /dev/null +++ b/core/modules/search/tests/src/Kernel/SearchPageValidationTest.php @@ -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(); + } + +} diff --git a/core/modules/shortcut/tests/src/Kernel/ShortcutSetValidationTest.php b/core/modules/shortcut/tests/src/Kernel/ShortcutSetValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..a27f3980d18658214687c2c138c8ace96d034e5d --- /dev/null +++ b/core/modules/shortcut/tests/src/Kernel/ShortcutSetValidationTest.php @@ -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(); + } + +} diff --git a/core/modules/system/tests/src/Kernel/Entity/ActionValidationTest.php b/core/modules/system/tests/src/Kernel/Entity/ActionValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..133d1df18977f43672d48eeee2ba9b790839043b --- /dev/null +++ b/core/modules/system/tests/src/Kernel/Entity/ActionValidationTest.php @@ -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(); + } + +} diff --git a/core/modules/system/tests/src/Kernel/Entity/MenuValidationTest.php b/core/modules/system/tests/src/Kernel/Entity/MenuValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..1c1ebd752b680e1708cc1fa3ccb01e9c148b92c5 --- /dev/null +++ b/core/modules/system/tests/src/Kernel/Entity/MenuValidationTest.php @@ -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(); + } + +} diff --git a/core/modules/taxonomy/tests/src/Kernel/VocabularyValidationTest.php b/core/modules/taxonomy/tests/src/Kernel/VocabularyValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..7de121d2dffe87efbf82e685cc9320634e093f8c --- /dev/null +++ b/core/modules/taxonomy/tests/src/Kernel/VocabularyValidationTest.php @@ -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(); + } + +} diff --git a/core/modules/user/tests/src/Kernel/RoleValidationTest.php b/core/modules/user/tests/src/Kernel/RoleValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..ff78efd37d1c976ec92f0e2c9f39afcaf7860c63 --- /dev/null +++ b/core/modules/user/tests/src/Kernel/RoleValidationTest.php @@ -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(); + } + +} diff --git a/core/modules/views/tests/src/Kernel/Entity/ViewValidationTest.php b/core/modules/views/tests/src/Kernel/Entity/ViewValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..94bc6023fc8bd6e2d67d3b7eb9207c14849fbaa3 --- /dev/null +++ b/core/modules/views/tests/src/Kernel/Entity/ViewValidationTest.php @@ -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(); + } + +} diff --git a/core/modules/workflows/tests/src/Kernel/WorkflowValidationTest.php b/core/modules/workflows/tests/src/Kernel/WorkflowValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..a3448a1a10cad69564d79631d9e3c477503a2710 --- /dev/null +++ b/core/modules/workflows/tests/src/Kernel/WorkflowValidationTest.php @@ -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(); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php new file mode 100644 index 0000000000000000000000000000000000000000..e3dcdffc6423d98b34c25721ba0b393fb7235ecd --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php @@ -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); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigExistsConstraintValidatorTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigExistsConstraintValidatorTest.php new file mode 100644 index 0000000000000000000000000000000000000000..03992fd9c92180d5906a921169657de3198526d1 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigExistsConstraintValidatorTest.php @@ -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()); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..e13c50d0eeaf2e2c416c900c1e36aeda752b2af8 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php @@ -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(); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Entity/DateFormatValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/DateFormatValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..10e97012bbdb8b35cfb76a0a43dcb6688d444588 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Entity/DateFormatValidationTest.php @@ -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(); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityFormDisplayValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityFormDisplayValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..cda1760508277b3a1b871aac42a5c340a3ca9180 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityFormDisplayValidationTest.php @@ -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(); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityFormModeValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityFormModeValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..31e31249bb20336d27b2d279ebeba6c469e711f7 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityFormModeValidationTest.php @@ -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(); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityViewDisplayValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityViewDisplayValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..b30998db1ac083a23eb47860bc975700514ee7aa --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityViewDisplayValidationTest.php @@ -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(); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityViewModeValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityViewModeValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..011a5e4844cc2275e9a8c6c68dea0234dbfb3a97 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityViewModeValidationTest.php @@ -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(); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Extension/ExtensionExistsConstraintValidatorTest.php b/core/tests/Drupal/KernelTests/Core/Extension/ExtensionExistsConstraintValidatorTest.php new file mode 100644 index 0000000000000000000000000000000000000000..f698279a56d6d1287f8883d234428bf61d6f4f46 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Extension/ExtensionExistsConstraintValidatorTest.php @@ -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(); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Extension/ExtensionNameConstraintTest.php b/core/tests/Drupal/KernelTests/Core/Extension/ExtensionNameConstraintTest.php new file mode 100644 index 0000000000000000000000000000000000000000..7ee79e95c16df481a8f6303de3a704dd34d45726 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Extension/ExtensionNameConstraintTest.php @@ -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()); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/TypedData/ValidKeysConstraintValidatorTest.php b/core/tests/Drupal/KernelTests/Core/TypedData/ValidKeysConstraintValidatorTest.php new file mode 100644 index 0000000000000000000000000000000000000000..e3877465e59cc5c4973fd4ccc77ea55ddcb9a94e --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/TypedData/ValidKeysConstraintValidatorTest.php @@ -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(); + } + +}