diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml index e6d7f243ac10..4476e98d0c7e 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: '' config_dependencies: type: config_dependencies_base @@ -263,6 +276,8 @@ config_dependencies: enforced: type: config_dependencies_base label: 'Enforced configuration dependencies' + constraints: + ValidKeys: '' 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 000000000000..006cebd464d4 --- /dev/null +++ b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ConfigExistsConstraint.php @@ -0,0 +1,26 @@ +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 000000000000..39bb4e184175 --- /dev/null +++ b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/RequiredConfigDependenciesConstraint.php @@ -0,0 +1,50 @@ +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 000000000000..3432531564e9 --- /dev/null +++ b/core/lib/Drupal/Core/Extension/Plugin/Validation/Constraint/ExtensionExistsConstraint.php @@ -0,0 +1,54 @@ +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 000000000000..5721ba0bfd2c --- /dev/null +++ b/core/lib/Drupal/Core/Extension/Plugin/Validation/Constraint/ExtensionNameConstraint.php @@ -0,0 +1,35 @@ +` 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 ``. + elseif ($this->allowedKeys === '') { + 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 000000000000..63562f5e80e9 --- /dev/null +++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ValidKeysConstraintValidator.php @@ -0,0 +1,42 @@ +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 000000000000..dd3683fd20c4 --- /dev/null +++ b/core/modules/block_content/tests/src/Kernel/BlockContentTypeValidationTest.php @@ -0,0 +1,33 @@ +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 000000000000..cc1891879c3e --- /dev/null +++ b/core/modules/comment/tests/src/Kernel/CommentTypeValidationTest.php @@ -0,0 +1,34 @@ +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 000000000000..b84b2ff626be --- /dev/null +++ b/core/modules/contact/tests/src/Kernel/ContactFormValidationTest.php @@ -0,0 +1,33 @@ +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 8b2d4fcea796..dd102d917975 100644 --- a/core/modules/editor/src/Entity/Editor.php +++ b/core/modules/editor/src/Entity/Editor.php @@ -30,6 +30,11 @@ use Drupal\editor\EditorInterface; * "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 000000000000..696060ffbf6a --- /dev/null +++ b/core/modules/editor/tests/src/Kernel/EditorValidationTest.php @@ -0,0 +1,65 @@ + '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 8067f5be5df0..66d88deb2d3b 100644 --- a/core/modules/field/src/Entity/FieldConfig.php +++ b/core/modules/field/src/Entity/FieldConfig.php @@ -44,6 +44,11 @@ use Drupal\field\FieldConfigInterface; * "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 000000000000..ee5cc5243757 --- /dev/null +++ b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php @@ -0,0 +1,56 @@ +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 000000000000..95ba5ec42bd8 --- /dev/null +++ b/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php @@ -0,0 +1,34 @@ +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 000000000000..0c8fb27ec440 --- /dev/null +++ b/core/modules/filter/tests/src/Kernel/FilterFormatValidationTest.php @@ -0,0 +1,33 @@ +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 000000000000..65e9288c449f --- /dev/null +++ b/core/modules/image/tests/src/Kernel/ImageStyleValidationTest.php @@ -0,0 +1,33 @@ +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 000000000000..7c95b3163cd0 --- /dev/null +++ b/core/modules/language/tests/src/Kernel/ConfigurableLanguageValidationTest.php @@ -0,0 +1,30 @@ +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 000000000000..01a2e6a77ed9 --- /dev/null +++ b/core/modules/language/tests/src/Kernel/ContentLanguageSettingsValidationTest.php @@ -0,0 +1,33 @@ +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 000000000000..0c6032afb6a5 --- /dev/null +++ b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayValidationTest.php @@ -0,0 +1,42 @@ + '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 000000000000..c5f2774b2c89 --- /dev/null +++ b/core/modules/media/tests/src/Kernel/MediaTypeValidationTest.php @@ -0,0 +1,30 @@ +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 000000000000..c5bcc47355e6 --- /dev/null +++ b/core/modules/node/tests/src/Kernel/NodeTypeValidationTest.php @@ -0,0 +1,31 @@ +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 000000000000..1c9a4cb98c92 --- /dev/null +++ b/core/modules/responsive_image/tests/src/Kernel/ResponsiveImageStyleValidationTest.php @@ -0,0 +1,33 @@ +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 000000000000..0d8cb96272cb --- /dev/null +++ b/core/modules/rest/tests/src/Kernel/Entity/RestResourceConfigValidationTest.php @@ -0,0 +1,36 @@ +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 000000000000..eb35264fd3f1 --- /dev/null +++ b/core/modules/search/tests/src/Kernel/SearchPageValidationTest.php @@ -0,0 +1,34 @@ +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 000000000000..a27f3980d186 --- /dev/null +++ b/core/modules/shortcut/tests/src/Kernel/ShortcutSetValidationTest.php @@ -0,0 +1,35 @@ +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 000000000000..133d1df18977 --- /dev/null +++ b/core/modules/system/tests/src/Kernel/Entity/ActionValidationTest.php @@ -0,0 +1,30 @@ +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 000000000000..1c1ebd752b68 --- /dev/null +++ b/core/modules/system/tests/src/Kernel/Entity/MenuValidationTest.php @@ -0,0 +1,28 @@ +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 000000000000..7de121d2dffe --- /dev/null +++ b/core/modules/taxonomy/tests/src/Kernel/VocabularyValidationTest.php @@ -0,0 +1,33 @@ +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 000000000000..ff78efd37d1c --- /dev/null +++ b/core/modules/user/tests/src/Kernel/RoleValidationTest.php @@ -0,0 +1,33 @@ +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 000000000000..94bc6023fc8b --- /dev/null +++ b/core/modules/views/tests/src/Kernel/Entity/ViewValidationTest.php @@ -0,0 +1,33 @@ +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 000000000000..a3448a1a10ca --- /dev/null +++ b/core/modules/workflows/tests/src/Kernel/WorkflowValidationTest.php @@ -0,0 +1,34 @@ +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 000000000000..e3dcdffc6423 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php @@ -0,0 +1,188 @@ +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 000000000000..03992fd9c921 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigExistsConstraintValidatorTest.php @@ -0,0 +1,45 @@ +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 000000000000..e13c50d0eeaf --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php @@ -0,0 +1,34 @@ +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 000000000000..10e97012bbdb --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Entity/DateFormatValidationTest.php @@ -0,0 +1,30 @@ +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 000000000000..cda176050827 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityFormDisplayValidationTest.php @@ -0,0 +1,31 @@ +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 000000000000..31e31249bb20 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityFormModeValidationTest.php @@ -0,0 +1,37 @@ +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 000000000000..b30998db1ac0 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityViewDisplayValidationTest.php @@ -0,0 +1,31 @@ +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 000000000000..011a5e4844cc --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityViewModeValidationTest.php @@ -0,0 +1,37 @@ +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 000000000000..f698279a56d6 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Extension/ExtensionExistsConstraintValidatorTest.php @@ -0,0 +1,65 @@ +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 000000000000..7ee79e95c16d --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Extension/ExtensionNameConstraintTest.php @@ -0,0 +1,43 @@ +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 000000000000..e3877465e59c --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/TypedData/ValidKeysConstraintValidatorTest.php @@ -0,0 +1,97 @@ +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', ''); + + $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 + // ``. + $config->getDataDefinition() + ->addConstraint('ValidKeys', 'infer'); + $this->expectExceptionMessage("'infer' is not a valid set of allowed keys."); + $config->validate(); + } + +}