From adfa35d41f2a53b9b32e5ce37d85bbf08cc9dc8d Mon Sep 17 00:00:00 2001
From: bnjmnm <benm@umich.edu>
Date: Fri, 2 Dec 2022 13:01:44 -0500
Subject: [PATCH] =?UTF-8?q?Issue=20#3324150=20by=20phenaproxima,=20Wim=20L?=
 =?UTF-8?q?eers,=20G=C3=A1bor=20Hojtsy:=20Add=20validation=20constraints?=
 =?UTF-8?q?=20to=20config=5Fentity.dependencies?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 core/config/schema/core.data_types.schema.yml |  15 ++
 .../Constraint/ConfigExistsConstraint.php     |  26 +++
 .../ConfigExistsConstraintValidator.php       |  51 +++++
 .../RequiredConfigDependenciesConstraint.php  |  50 +++++
 ...dConfigDependenciesConstraintValidator.php |  80 ++++++++
 .../Constraint/ExtensionExistsConstraint.php  |  54 +++++
 .../ExtensionExistsConstraintValidator.php    |  80 ++++++++
 .../Constraint/ExtensionNameConstraint.php    |  35 ++++
 .../Constraint/ValidKeysConstraint.php        |  97 +++++++++
 .../ValidKeysConstraintValidator.php          |  42 ++++
 .../Kernel/BlockContentTypeValidationTest.php |  33 +++
 .../src/Kernel/CommentTypeValidationTest.php  |  34 ++++
 .../src/Kernel/ContactFormValidationTest.php  |  33 +++
 core/modules/editor/src/Entity/Editor.php     |   5 +
 .../tests/src/Kernel/EditorValidationTest.php |  65 ++++++
 core/modules/field/src/Entity/FieldConfig.php |   5 +
 .../Entity/FieldConfigValidationTest.php      |  56 ++++++
 .../FieldStorageConfigValidationTest.php      |  34 ++++
 .../src/Kernel/FilterFormatValidationTest.php |  33 +++
 .../src/Kernel/ImageStyleValidationTest.php   |  33 +++
 .../ConfigurableLanguageValidationTest.php    |  30 +++
 .../ContentLanguageSettingsValidationTest.php |  33 +++
 ...BuilderEntityViewDisplayValidationTest.php |  42 ++++
 .../src/Kernel/MediaTypeValidationTest.php    |  30 +++
 .../src/Kernel/NodeTypeValidationTest.php     |  31 +++
 .../ResponsiveImageStyleValidationTest.php    |  33 +++
 .../RestResourceConfigValidationTest.php      |  36 ++++
 .../src/Kernel/SearchPageValidationTest.php   |  34 ++++
 .../src/Kernel/ShortcutSetValidationTest.php  |  35 ++++
 .../Kernel/Entity/ActionValidationTest.php    |  30 +++
 .../src/Kernel/Entity/MenuValidationTest.php  |  28 +++
 .../src/Kernel/VocabularyValidationTest.php   |  33 +++
 .../tests/src/Kernel/RoleValidationTest.php   |  33 +++
 .../src/Kernel/Entity/ViewValidationTest.php  |  33 +++
 .../src/Kernel/WorkflowValidationTest.php     |  34 ++++
 .../Config/ConfigEntityValidationTestBase.php | 188 ++++++++++++++++++
 .../ConfigExistsConstraintValidatorTest.php   |  45 +++++
 .../BaseFieldOverrideValidationTest.php       |  34 ++++
 .../Core/Entity/DateFormatValidationTest.php  |  30 +++
 .../EntityFormDisplayValidationTest.php       |  31 +++
 .../Entity/EntityFormModeValidationTest.php   |  37 ++++
 .../EntityViewDisplayValidationTest.php       |  31 +++
 .../Entity/EntityViewModeValidationTest.php   |  37 ++++
 ...ExtensionExistsConstraintValidatorTest.php |  65 ++++++
 .../Extension/ExtensionNameConstraintTest.php |  43 ++++
 .../ValidKeysConstraintValidatorTest.php      |  97 +++++++++
 46 files changed, 1994 insertions(+)
 create mode 100644 core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ConfigExistsConstraint.php
 create mode 100644 core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ConfigExistsConstraintValidator.php
 create mode 100644 core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/RequiredConfigDependenciesConstraint.php
 create mode 100644 core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/RequiredConfigDependenciesConstraintValidator.php
 create mode 100644 core/lib/Drupal/Core/Extension/Plugin/Validation/Constraint/ExtensionExistsConstraint.php
 create mode 100644 core/lib/Drupal/Core/Extension/Plugin/Validation/Constraint/ExtensionExistsConstraintValidator.php
 create mode 100644 core/lib/Drupal/Core/Extension/Plugin/Validation/Constraint/ExtensionNameConstraint.php
 create mode 100644 core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ValidKeysConstraint.php
 create mode 100644 core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ValidKeysConstraintValidator.php
 create mode 100644 core/modules/block_content/tests/src/Kernel/BlockContentTypeValidationTest.php
 create mode 100644 core/modules/comment/tests/src/Kernel/CommentTypeValidationTest.php
 create mode 100644 core/modules/contact/tests/src/Kernel/ContactFormValidationTest.php
 create mode 100644 core/modules/editor/tests/src/Kernel/EditorValidationTest.php
 create mode 100644 core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
 create mode 100644 core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
 create mode 100644 core/modules/filter/tests/src/Kernel/FilterFormatValidationTest.php
 create mode 100644 core/modules/image/tests/src/Kernel/ImageStyleValidationTest.php
 create mode 100644 core/modules/language/tests/src/Kernel/ConfigurableLanguageValidationTest.php
 create mode 100644 core/modules/language/tests/src/Kernel/ContentLanguageSettingsValidationTest.php
 create mode 100644 core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayValidationTest.php
 create mode 100644 core/modules/media/tests/src/Kernel/MediaTypeValidationTest.php
 create mode 100644 core/modules/node/tests/src/Kernel/NodeTypeValidationTest.php
 create mode 100644 core/modules/responsive_image/tests/src/Kernel/ResponsiveImageStyleValidationTest.php
 create mode 100644 core/modules/rest/tests/src/Kernel/Entity/RestResourceConfigValidationTest.php
 create mode 100644 core/modules/search/tests/src/Kernel/SearchPageValidationTest.php
 create mode 100644 core/modules/shortcut/tests/src/Kernel/ShortcutSetValidationTest.php
 create mode 100644 core/modules/system/tests/src/Kernel/Entity/ActionValidationTest.php
 create mode 100644 core/modules/system/tests/src/Kernel/Entity/MenuValidationTest.php
 create mode 100644 core/modules/taxonomy/tests/src/Kernel/VocabularyValidationTest.php
 create mode 100644 core/modules/user/tests/src/Kernel/RoleValidationTest.php
 create mode 100644 core/modules/views/tests/src/Kernel/Entity/ViewValidationTest.php
 create mode 100644 core/modules/workflows/tests/src/Kernel/WorkflowValidationTest.php
 create mode 100644 core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php
 create mode 100644 core/tests/Drupal/KernelTests/Core/Config/ConfigExistsConstraintValidatorTest.php
 create mode 100644 core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php
 create mode 100644 core/tests/Drupal/KernelTests/Core/Entity/DateFormatValidationTest.php
 create mode 100644 core/tests/Drupal/KernelTests/Core/Entity/EntityFormDisplayValidationTest.php
 create mode 100644 core/tests/Drupal/KernelTests/Core/Entity/EntityFormModeValidationTest.php
 create mode 100644 core/tests/Drupal/KernelTests/Core/Entity/EntityViewDisplayValidationTest.php
 create mode 100644 core/tests/Drupal/KernelTests/Core/Entity/EntityViewModeValidationTest.php
 create mode 100644 core/tests/Drupal/KernelTests/Core/Extension/ExtensionExistsConstraintValidatorTest.php
 create mode 100644 core/tests/Drupal/KernelTests/Core/Extension/ExtensionNameConstraintTest.php
 create mode 100644 core/tests/Drupal/KernelTests/Core/TypedData/ValidKeysConstraintValidatorTest.php

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: '<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 000000000000..006cebd464d4
--- /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 000000000000..590a3056a110
--- /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 000000000000..39bb4e184175
--- /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 000000000000..17802873cae6
--- /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 000000000000..3432531564e9
--- /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 000000000000..404d5dcf38c2
--- /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 000000000000..5721ba0bfd2c
--- /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 000000000000..842615c9ea24
--- /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 000000000000..63562f5e80e9
--- /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 000000000000..dd3683fd20c4
--- /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 000000000000..cc1891879c3e
--- /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 000000000000..b84b2ff626be
--- /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 8b2d4fcea796..dd102d917975 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 000000000000..696060ffbf6a
--- /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 8067f5be5df0..66d88deb2d3b 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 000000000000..ee5cc5243757
--- /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 000000000000..95ba5ec42bd8
--- /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 000000000000..0c8fb27ec440
--- /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 000000000000..65e9288c449f
--- /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 000000000000..7c95b3163cd0
--- /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 000000000000..01a2e6a77ed9
--- /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 000000000000..0c6032afb6a5
--- /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 000000000000..c5f2774b2c89
--- /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 000000000000..c5bcc47355e6
--- /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 000000000000..1c9a4cb98c92
--- /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 000000000000..0d8cb96272cb
--- /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 000000000000..eb35264fd3f1
--- /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 000000000000..a27f3980d186
--- /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 000000000000..133d1df18977
--- /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 000000000000..1c1ebd752b68
--- /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 000000000000..7de121d2dffe
--- /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 000000000000..ff78efd37d1c
--- /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 000000000000..94bc6023fc8b
--- /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 000000000000..a3448a1a10ca
--- /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 000000000000..e3dcdffc6423
--- /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 000000000000..03992fd9c921
--- /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 000000000000..e13c50d0eeaf
--- /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 000000000000..10e97012bbdb
--- /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 000000000000..cda176050827
--- /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 000000000000..31e31249bb20
--- /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 000000000000..b30998db1ac0
--- /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 000000000000..011a5e4844cc
--- /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 000000000000..f698279a56d6
--- /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 000000000000..7ee79e95c16d
--- /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 000000000000..e3877465e59c
--- /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();
+  }
+
+}
-- 
GitLab