Initial Drupal 11 with DDEV setup

This commit is contained in:
gluebox
2025-10-08 11:39:17 -04:00
commit 89ef74b305
25344 changed files with 2599172 additions and 0 deletions

View File

@ -0,0 +1,245 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Config;
use Drupal\Core\Config\Entity\ConfigEntityDependency;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Config\InstallStorage;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Extension\ExtensionLifecycle;
use Drupal\KernelTests\AssertConfigTrait;
use Drupal\KernelTests\FileSystemModuleDiscoveryDataProviderTrait;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests that the installed config matches the default config.
*
* @group Config
* @group #slow
*/
class DefaultConfigTest extends KernelTestBase {
use AssertConfigTrait;
use FileSystemModuleDiscoveryDataProviderTrait;
/**
* {@inheritdoc}
*/
protected static $timeLimit = 500;
/**
* {@inheritdoc}
*/
protected static $modules = ['system', 'user', 'path_alias'];
/**
* The following config entries are changed on module install.
*
* Comparing them does not make sense.
*
* @var array
*/
public static $skippedConfig = [
'locale.settings' => ['path: '],
'syslog.settings' => ['facility: '],
];
/**
* Tests if installed config is equal to the exported config.
*
* @dataProvider moduleListDataProvider
*/
public function testModuleConfig(string $module): void {
$this->assertExtensionConfig($module, 'module');
}
/**
* Tests if installed config is equal to the exported config.
*
* @dataProvider themeListDataProvider
*/
public function testThemeConfig($theme): void {
$this->assertExtensionConfig($theme, 'theme');
}
/**
* Tests that the config provided by the extension is correct.
*
* @param string $name
* Extension name.
* @param string $type
* Extension type, either 'module' or 'theme'.
*
* @internal
*/
protected function assertExtensionConfig(string $name, string $type): void {
// Parse .info.yml file for module/theme $name. Since it's not installed at
// this point we can't retrieve it from the 'module_handler' service.
switch ($name) {
case 'test_deprecated_theme':
$file_name = DRUPAL_ROOT . '/core/modules/system/tests/themes/' . $name . '/' . $name . '.info.yml';
break;
case 'deprecated_module':
$file_name = DRUPAL_ROOT . '/core/modules/system/tests/modules/' . $name . '/' . $name . '.info.yml';
break;
default:
$file_name = DRUPAL_ROOT . '/core/' . $type . 's/' . $name . '/' . $name . '.info.yml';
}
$info = \Drupal::service('info_parser')->parse($file_name);
// Test we have a parsed info.yml file.
$this->assertNotEmpty($info);
// Skip deprecated extensions.
if (isset($info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER])
&& $info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::DEPRECATED) {
$this->markTestSkipped("The $type '$name' is deprecated.");
}
// System and user are required in order to be able to install some of the
// other modules. Therefore they are put into static::$modules, which though
// doesn't install config files, so import those config files explicitly. Do
// this for all tests in case optional configuration depends on it.
$this->installConfig(['system', 'user']);
$extension_path = \Drupal::service('extension.path.resolver')->getPath($type, $name) . '/';
$extension_config_storage = new FileStorage($extension_path . InstallStorage::CONFIG_INSTALL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION);
$optional_config_storage = new FileStorage($extension_path . InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION);
if (empty($optional_config_storage->listAll()) && empty($extension_config_storage->listAll())) {
$this->markTestSkipped("$name has no configuration to test");
}
// Work out any additional modules and themes that need installing to create
// an optional config.
$modules_to_install = $type !== 'theme' ? [$name] : [];
$themes_to_install = $type === 'theme' ? [$name] : [];
foreach ($optional_config_storage->listAll() as $config_name) {
$data = $optional_config_storage->read($config_name);
$dependency = new ConfigEntityDependency($config_name, $data);
$modules_to_install = array_merge($modules_to_install, $dependency->getDependencies('module'));
$themes_to_install = array_merge($themes_to_install, $dependency->getDependencies('theme'));
}
// Remove core and standard because they cannot be installed.
$modules_to_install = array_diff(array_unique($modules_to_install), ['core', 'standard']);
$this->container->get('module_installer')->install($modules_to_install);
$this->container->get('theme_installer')->install(array_unique($themes_to_install));
// Test configuration in the extension's config/install directory.
$this->doTestsOnConfigStorage($extension_config_storage, $name, $type);
// Test configuration in the extension's config/optional directory.
$this->doTestsOnConfigStorage($optional_config_storage, $name, $type);
}
/**
* A data provider that lists every theme in core.
*
* Also adds a deprecated theme with config.
*
* @return string[][]
* An array of theme names to test, with both key and value being the name
* of the theme.
*/
public static function themeListDataProvider() {
$prefix = dirname(__DIR__, 4) . DIRECTORY_SEPARATOR . 'themes';
$theme_dirs = array_keys(iterator_to_array(new \FilesystemIterator($prefix)));
$theme_names = array_map(function ($path) use ($prefix) {
return str_replace($prefix . DIRECTORY_SEPARATOR, '', $path);
}, $theme_dirs);
$themes_keyed = array_combine($theme_names, $theme_names);
// Engines is not a theme.
unset($themes_keyed['engines']);
// Add a deprecated theme with config.
$themes_keyed['test_deprecated_theme'] = 'test_deprecated_theme';
return array_map(function ($theme) {
return [$theme];
}, $themes_keyed);
}
/**
* A data provider that lists every module in core.
*
* Also adds a deprecated module with config.
*
* @return string[][]
* An array of module names to test, with both key and value being the name
* of the module.
*/
public static function moduleListDataProvider(): array {
$modules_keyed = self::coreModuleListDataProvider();
// Add a deprecated module with config.
$modules_keyed['deprecated_module'] = ['deprecated_module'];
return $modules_keyed;
}
/**
* Tests that default config matches the installed config.
*
* @param \Drupal\Core\Config\StorageInterface $default_config_storage
* The default config storage to test.
* @param string $extension
* The extension that is being tested.
* @param string $type
* The extension type to test.
*/
protected function doTestsOnConfigStorage(StorageInterface $default_config_storage, $extension, string $type = 'module'): void {
/** @var \Drupal\Core\Config\ConfigManagerInterface $config_manager */
$config_manager = $this->container->get('config.manager');
// Just connect directly to the config table so we don't need to worry about
// the cache layer.
$active_config_storage = $this->container->get('config.storage');
/** @var \Drupal\Core\Config\ConfigFactoryInterface $config_factory */
$config_factory = $this->container->get('config.factory');
foreach ($default_config_storage->listAll() as $config_name) {
if ($active_config_storage->exists($config_name)) {
// If it is a config entity re-save it. This ensures that any
// recalculation of dependencies does not cause config change.
if ($entity_type = $config_manager->getEntityTypeIdByName($config_name)) {
$entity_storage = $config_manager
->getEntityTypeManager()
->getStorage($entity_type);
$id = $entity_storage->getIDFromConfigName($config_name, $entity_storage->getEntityType()
->getConfigPrefix());
$entity_storage->load($id)->calculateDependencies()->save();
}
else {
// Ensure simple configuration is re-saved so any schema sorting is
// applied.
$config_factory->getEditable($config_name)->save();
}
$result = $config_manager->diff($default_config_storage, $active_config_storage, $config_name);
// ::assertConfigDiff will throw an exception if the configuration is
// different.
$this->assertNull($this->assertConfigDiff($result, $config_name, static::$skippedConfig));
}
else {
$data = $default_config_storage->read($config_name);
$dependency = new ConfigEntityDependency($config_name, $data);
if ($dependency->hasDependency('module', 'standard')) {
// Skip configuration with a dependency on the standard profile. Such
// configuration has probably been removed from the standard profile
// and needs its own test.
continue;
}
$info = $this->container->get("extension.list.$type")->getExtensionInfo($extension);
if (!isset($info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER]) || $info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] !== ExtensionLifecycle::EXPERIMENTAL) {
$this->fail("$config_name provided by $extension does not exist after installing all dependencies");
}
}
}
}
}

View File

@ -0,0 +1,564 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Config\Schema;
// cspell:ignore childkey
use Drupal\block\Entity\Block;
use Drupal\Core\Config\Schema\Mapping;
use Drupal\Core\TypedData\MapDataDefinition;
use Drupal\editor\Entity\Editor;
use Drupal\field\Entity\FieldConfig;
use Drupal\filter\Entity\FilterFormat;
use Drupal\KernelTests\KernelTestBase;
/**
* @coversDefaultClass \Drupal\Core\Config\Schema\Mapping
* @group Config
*/
class MappingTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $configSchemaCheckerExclusions = [
'config_schema_deprecated_test.settings',
];
/**
* @dataProvider providerMappingInterpretation
*/
public function testMappingInterpretation(
string $config_name,
?string $property_path,
array $expected_valid_keys,
array $expected_optional_keys,
array $expected_dynamically_valid_keys,
): void {
// Some config needs some dependencies installed.
switch ($config_name) {
case 'block.block.branding':
$this->enableModules(['system', 'block']);
/** @var \Drupal\Core\Extension\ThemeInstallerInterface $theme_installer */
$theme_installer = $this->container->get('theme_installer');
$theme_installer->install(['stark']);
Block::create([
'id' => 'branding',
'plugin' => 'system_branding_block',
'theme' => 'stark',
'status' => TRUE,
'settings' => [
'use_site_logo' => TRUE,
'use_site_name' => TRUE,
'use_site_slogan' => TRUE,
'label_display' => FALSE,
// This is inherited from `type: block_settings`.
'context_mapping' => [],
],
])->save();
break;
case 'block.block.local_tasks':
$this->enableModules(['system', 'block']);
/** @var \Drupal\Core\Extension\ThemeInstallerInterface $theme_installer */
$theme_installer = $this->container->get('theme_installer');
$theme_installer->install(['stark']);
Block::create([
'id' => 'local_tasks',
'plugin' => 'local_tasks_block',
'theme' => 'stark',
'status' => TRUE,
'settings' => [
'primary' => TRUE,
'secondary' => FALSE,
// This is inherited from `type: block_settings`.
'context_mapping' => [],
],
])->save();
break;
case 'block.block.positively_powered___alternate_reality_with_fallback_type___':
$this->enableModules(['config_schema_add_fallback_type_test']);
$id = 'positively_powered___alternate_reality_with_fallback_type___';
case 'block.block.positively_powered':
$this->enableModules(['system', 'block']);
/** @var \Drupal\Core\Extension\ThemeInstallerInterface $theme_installer */
$theme_installer = $this->container->get('theme_installer');
$theme_installer->install(['stark']);
Block::create([
'id' => $id ?? 'positively_powered',
'plugin' => 'system_powered_by_block',
'theme' => 'stark',
'status' => TRUE,
'settings' => [
'label_display' => FALSE,
// This is inherited from `type: block_settings`.
'context_mapping' => [],
],
// Avoid showing "Powered by Drupal" on 404 responses.
'visibility' => [
'I_CAN_CHOOSE_THIS' => [
// This is what determines the
'id' => 'response_status',
'negate' => FALSE,
'status_codes' => [
404,
],
],
],
])->save();
break;
case 'config_schema_deprecated_test.settings':
$this->enableModules(['config_schema_deprecated_test']);
$config = $this->config('config_schema_deprecated_test.settings');
// @see \Drupal\KernelTests\Core\Config\ConfigSchemaDeprecationTest
$config
->set('complex_structure_deprecated.type', 'fruits')
->set('complex_structure_deprecated.products', ['apricot', 'apple'])
->save();
break;
case 'editor.editor.funky':
$this->enableModules(['filter', 'editor', 'ckeditor5']);
FilterFormat::create(['format' => 'funky', 'name' => 'Funky'])->save();
Editor::create([
'format' => 'funky',
'editor' => 'ckeditor5',
'image_upload' => [
'status' => FALSE,
],
])->save();
break;
case 'field.field.node.config_mapping_test.comment_config_mapping_test':
$this->enableModules(['user', 'field', 'node', 'comment', 'taxonomy', 'config_mapping_test']);
$this->installEntitySchema('user');
$this->installEntitySchema('node');
$this->assertNull(FieldConfig::load('node.config_mapping_test.comment_config_mapping_test'));
// \Drupal\node\Entity\NodeType::$preview_mode uses DRUPAL_OPTIONAL,
// which is defined in system.module.
require_once 'core/modules/system/system.module';
$this->installConfig(['config_mapping_test']);
$this->assertNotNull(FieldConfig::load('node.config_mapping_test.comment_config_mapping_test'));
break;
}
/** @var \Drupal\Core\Config\TypedConfigManagerInterface $typed_config_manager */
$typed_config_manager = \Drupal::service('config.typed');
$mapping = $typed_config_manager->get($config_name);
if ($property_path) {
$mapping = $mapping->get($property_path);
}
assert($mapping instanceof Mapping);
$expected_required_keys = array_values(array_diff($expected_valid_keys, $expected_optional_keys));
$this->assertSame($expected_valid_keys, $mapping->getValidKeys());
$this->assertSame($expected_required_keys, $mapping->getRequiredKeys());
$this->assertSame($expected_dynamically_valid_keys, $mapping->getDynamicallyValidKeys());
$this->assertSame($expected_optional_keys, $mapping->getOptionalKeys());
}
/**
* Provides test cases for all kinds of i) dynamic typing, ii) optional keys.
*
* @see https://www.drupal.org/files/ConfigSchemaCheatSheet2.0.pdf
*
* @return \Generator
* The test cases.
*/
public static function providerMappingInterpretation(): \Generator {
$available_block_settings_types = [
'block.settings.field_block:*:*:*' => [
'formatter',
],
'block.settings.extra_field_block:*:*:*' => [
'formatter',
],
'block.settings.system_branding_block' => [
'use_site_logo',
'use_site_name',
'use_site_slogan',
],
'block.settings.system_menu_block:*' => [
'level',
'depth',
'expand_all_items',
],
'block.settings.local_tasks_block' => [
'primary',
'secondary',
],
];
// A simple config often is just a single Mapping object.
yield 'No dynamic type: core.extension' => [
'core.extension',
NULL,
[
// Keys inherited from `type: config_object`.
// @see core/config/schema/core.data_types.schema.yml
'_core',
'langcode',
// Keys defined locally, in `type: core.extension`.
// @see core/config/schema/core.extension.schema.yml
'module',
'theme',
'profile',
],
[
'_core',
'langcode',
'profile',
],
[],
];
// Special case: deprecated is needed for deprecated config schema:
// - deprecated keys are treated as optional
// - if a deprecated property path is itself a mapping, then the keys inside
// are not optional
yield 'No dynamic type: config_schema_deprecated_test.settings' => [
'config_schema_deprecated_test.settings',
NULL,
[
// Keys inherited from `type: config_object`.
// @see core/config/schema/core.data_types.schema.yml
'_core',
'langcode',
// Keys defined locally, in `type:
// config_schema_deprecated_test.settings`.
// @see core/modules/config/tests/config_schema_deprecated_test/config/schema/config_schema_deprecated_test.schema.yml
'complex_structure_deprecated',
],
['_core', 'langcode', 'complex_structure_deprecated'],
[],
];
yield 'No dynamic type: config_schema_deprecated_test.settings:complex_structure_deprecated' => [
'config_schema_deprecated_test.settings',
'complex_structure_deprecated',
[
// Keys defined locally, in `type:
// config_schema_deprecated_test.settings`.
// @see core/modules/config/tests/config_schema_deprecated_test/config/schema/config_schema_deprecated_test.schema.yml
'type',
'products',
],
[],
[],
];
// A config entity is always a Mapping at the top level, but most nesting is
// also using Mappings (unless the keys are free to be chosen, then a
// Sequence would be used).
yield 'No dynamic type: block.block.branding' => [
'block.block.branding',
NULL,
[
// Keys inherited from `type: config_entity`.
// @see core/config/schema/core.data_types.schema.yml
'uuid',
'langcode',
'status',
'dependencies',
'third_party_settings',
'_core',
// Keys defined locally, in `type: block.block.*`.
// @see core/modules/block/config/schema/block.schema.yml
'id',
'theme',
'region',
'weight',
'provider',
'plugin',
'settings',
'visibility',
],
['third_party_settings', '_core'],
[],
];
// An example of nested Mapping objects in config entities.
yield 'No dynamic type: block.block.branding:dependencies' => [
'block.block.branding',
'dependencies',
[
// Keys inherited from `type: config_dependencies_base`.
// @see core/config/schema/core.data_types.schema.yml
'config',
'content',
'module',
'theme',
// Keys defined locally, in `type: config_dependencies`.
// @see core/config/schema/core.data_types.schema.yml
'enforced',
],
// All these keys are optional!
['config', 'content', 'module', 'theme', 'enforced'],
[],
];
// Three examples of `[%parent]`-based dynamic typing in config schema, and
// the consequences on what keys are considered valid: the first 2 depend
// on the block plugin being used using a single `%parent`, the third
// depends on the field plugin being used using a double `%parent`.
// See `type: block.block.*` which uses
// `type: block.settings.[%parent.plugin]`, and `type: field_config_base`
// which uses `type: field.value.[%parent.%parent.field_type]`.
yield 'Dynamic type with [%parent]: block.block.branding:settings' => [
'block.block.branding',
'settings',
[
// Keys inherited from `type: block.settings.*`, which in turn is
// inherited from `type: block_settings`.
// @see core/config/schema/core.data_types.schema.yml
'id',
'label',
'label_display',
'provider',
'context_mapping',
// Keys defined locally, in `type:
// block.settings.system_branding_block`.
// @see core/modules/block/config/schema/block.schema.yml
...$available_block_settings_types['block.settings.system_branding_block'],
],
// This key is optional, see `type: block_settings`.
// @see core.data_types.schema.yml
['context_mapping'],
$available_block_settings_types,
];
yield 'Dynamic type with [%parent]: block.block.local_tasks:settings' => [
'block.block.local_tasks',
'settings',
[
// Keys inherited from `type: block.settings.*`, which in turn is
// inherited from `type: block_settings`.
// @see core/config/schema/core.data_types.schema.yml
'id',
'label',
'label_display',
'provider',
'context_mapping',
// Keys defined locally, in `type: block.settings.local_tasks_block`.
// @see core/modules/system/config/schema/system.schema.yml
...$available_block_settings_types['block.settings.local_tasks_block'],
],
// This key is optional, see `type: block_settings`.
// @see core.data_types.schema.yml
['context_mapping'],
$available_block_settings_types,
];
yield 'Dynamic type with [%parent.%parent]: field.field.node.config_mapping_test.comment_config_mapping_test:default_value.0' => [
'field.field.node.config_mapping_test.comment_config_mapping_test',
'default_value.0',
[
// Keys defined locally, in `type: field.value.comment`.
// @see core/modules/comment/config/schema/comment.schema.yml
'status',
'cid',
'last_comment_timestamp',
'last_comment_name',
'last_comment_uid',
'comment_count',
],
[],
[
'field.value.string' => ['value'],
'field.value.string_long' => ['value'],
'field.value.uri' => ['value'],
'field.value.created' => ['value'],
'field.value.changed' => ['value'],
'field.value.entity_reference' => ['target_id', 'target_uuid'],
'field.value.boolean' => ['value'],
'field.value.email' => ['value'],
'field.value.integer' => ['value'],
'field.value.decimal' => ['value'],
'field.value.float' => ['value'],
'field.value.timestamp' => ['value'],
'field.value.language' => ['value'],
'field.value.comment' => [
'status',
'cid',
'last_comment_timestamp',
'last_comment_name',
'last_comment_uid',
'comment_count',
],
],
];
// An example of `[childkey]`-based dynamic mapping typing in config schema,
// for a mapping inside a sequence: the `id` key-value pair in the mapping
// determines the type of the mapping. The key in the sequence whose value
// is the mapping is irrelevant, it can be arbitrarily chosen.
// See `type: block.block.*` which uses `type: condition.plugin.[id]`.
yield 'Dynamic type with [childkey]: block.block.positively_powered:visibility.I_CAN_CHOOSE_THIS' => [
'block.block.positively_powered',
'visibility.I_CAN_CHOOSE_THIS',
[
// Keys inherited from `type: condition.plugin`.
// @see core/config/schema/core.data_types.schema.yml
'id',
'negate',
'uuid',
'context_mapping',
// Keys defined locally, in `type: condition.plugin.response_status`.
// @see core/modules/system/config/schema/system.schema.yml
'status_codes',
],
[],
// Note the presence of `id`, `negate`, `uuid` and `context_mapping` here.
// That's because there is no `condition.plugin.*` type that specifies
// defaults. Each individual condition plugin has the freedom to deviate
// from this approach!
[
'condition.plugin.entity_bundle:*' => [
'id',
'negate',
'uuid',
'context_mapping',
'bundles',
],
'condition.plugin.request_path' => [
'id',
'negate',
'uuid',
'context_mapping',
'pages',
],
'condition.plugin.response_status' => [
'id',
'negate',
'uuid',
'context_mapping',
'status_codes',
],
'condition.plugin.current_theme' => [
'id',
'negate',
'uuid',
'context_mapping',
'theme',
],
],
];
// Same, but what if `type: condition.plugin.*` would have existed?
// @see core/modules/config/tests/config_schema_add_fallback_type_test/config/schema/config_schema_add_fallback_type_test.schema.yml
yield 'Dynamic type with [childkey]: block.block.positively_powered___alternate_reality_with_fallback_type___:visibility' => [
'block.block.positively_powered___alternate_reality_with_fallback_type___',
'visibility.I_CAN_CHOOSE_THIS',
[
// Keys inherited from `type: condition.plugin`.
// @see core/config/schema/core.data_types.schema.yml
'id',
'negate',
'uuid',
'context_mapping',
// Keys defined locally, in `type: condition.plugin.response_status`.
// @see core/modules/system/config/schema/system.schema.yml
'status_codes',
],
[],
// Note the ABSENCE of `id`, `negate`, `uuid` and `context_mapping`
// compared to the previous test case, because now the
// `condition.plugin.*` type does exist.
[
'condition.plugin.entity_bundle:*' => [
'bundles',
],
'condition.plugin.request_path' => [
'pages',
],
'condition.plugin.response_status' => [
'status_codes',
],
'condition.plugin.current_theme' => [
'theme',
],
],
];
// An example of `[%key]`-based dynamic mapping typing in config schema: the
// key in the sequence determines the type of the mapping. Unlike the above
// `[childkey]` example, the key has meaning here.
// See `type: editor.settings.ckeditor5`, which uses
// `type: ckeditor5.plugin.[%key]`.
yield 'Dynamic type with [%key]: editor.editor.funky:settings.plugins.ckeditor5_heading' => [
'editor.editor.funky',
'settings.plugins.ckeditor5_heading',
[
// Keys defined locally, in `type: ckeditor5.plugin.ckeditor5_heading`.
// @see core/modules/ckeditor5/config/schema/ckeditor5.schema.yml
'enabled_headings',
],
[],
[
'ckeditor5.plugin.ckeditor5_language' => ['language_list'],
'ckeditor5.plugin.ckeditor5_heading' => ['enabled_headings'],
'ckeditor5.plugin.ckeditor5_imageResize' => ['allow_resize'],
'ckeditor5.plugin.ckeditor5_sourceEditing' => ['allowed_tags'],
'ckeditor5.plugin.ckeditor5_alignment' => ['enabled_alignments'],
'ckeditor5.plugin.ckeditor5_list' => ['properties', 'multiBlock'],
'ckeditor5.plugin.media_media' => ['allow_view_mode_override'],
'ckeditor5.plugin.ckeditor5_codeBlock' => ['languages'],
'ckeditor5.plugin.ckeditor5_style' => ['styles'],
],
];
}
/**
* @testWith [false, 42, "The mapping definition at `foobar` is invalid: its `invalid` key contains a integer. It must be an array."]
* [false, 10.2, "The mapping definition at `foobar` is invalid: its `invalid` key contains a double. It must be an array."]
* [false, "type", "The mapping definition at `foobar` is invalid: its `invalid` key contains a string. It must be an array."]
* [false, false, "The mapping definition at `foobar` is invalid: its `invalid` key contains a boolean. It must be an array."]
* [true, 42, "The mapping definition at `my_module.settings:foobar` is invalid: its `invalid` key contains a integer. It must be an array."]
* [true, 10.2, "The mapping definition at `my_module.settings:foobar` is invalid: its `invalid` key contains a double. It must be an array."]
* [true, "type", "The mapping definition at `my_module.settings:foobar` is invalid: its `invalid` key contains a string. It must be an array."]
* [true, false, "The mapping definition at `my_module.settings:foobar` is invalid: its `invalid` key contains a boolean. It must be an array."]
*/
public function testInvalidMappingKeyDefinition(bool $has_parent, mixed $invalid_key_definition, string $expected_message): void {
$definition = new MapDataDefinition([
'type' => 'mapping',
'mapping' => [
'valid' => [
'type' => 'boolean',
'label' => 'This is a valid key-value pair in this mapping',
],
'invalid' => $invalid_key_definition,
],
]);
$parent = NULL;
if ($has_parent) {
$parent = new Mapping(
new MapDataDefinition(['type' => 'mapping', 'mapping' => []]),
'my_module.settings',
);
}
$this->expectException(\LogicException::class);
$this->expectExceptionMessage($expected_message);
new Mapping($definition, 'foobar', $parent);
}
/**
* @testWith [true]
* [1]
* ["true"]
* [0]
* ["false"]
*/
public function testInvalidRequiredKeyFlag(mixed $required_key_flag_value): void {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('The `requiredKey` flag must either be omitted or have `false` as the value.');
new Mapping(new MapDataDefinition([
'type' => 'mapping',
'mapping' => [
'something' => [
'requiredKey' => $required_key_flag_value,
],
],
]));
}
}

View File

@ -0,0 +1,231 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Config;
use Drupal\Core\Config\Schema\Sequence;
use Drupal\Core\Config\Schema\SequenceDataDefinition;
use Drupal\Core\Config\Schema\TypedConfigInterface;
use Drupal\Core\TypedData\ComplexDataDefinitionInterface;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\TypedData\Type\IntegerInterface;
use Drupal\Core\TypedData\Type\StringInterface;
use Drupal\KernelTests\KernelTestBase;
use Symfony\Component\Validator\ConstraintViolationListInterface;
// cspell:ignore nyans
/**
* Tests config validation mechanism.
*
* @group Config
*/
class TypedConfigTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['config_test'];
/**
* {@inheritdoc}
*/
protected static $configSchemaCheckerExclusions = ['config_test.validation'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig('config_test');
}
/**
* Verifies that the Typed Data API is implemented correctly.
*/
public function testTypedDataAPI(): void {
/** @var \Drupal\Core\Config\TypedConfigManagerInterface $typed_config_manager */
$typed_config_manager = \Drupal::service('config.typed');
// Test non-existent data.
try {
$typed_config_manager->get('config_test.non_existent');
$this->fail('Expected error when trying to get non-existent typed config.');
}
catch (\InvalidArgumentException $e) {
$this->assertEquals('Missing required data for typed configuration: config_test.non_existent', $e->getMessage());
}
/** @var \Drupal\Core\Config\Schema\TypedConfigInterface $typed_config */
$typed_config = $typed_config_manager->get('config_test.validation');
// Test a primitive.
$string_data = $typed_config->get('llama');
$this->assertInstanceOf(StringInterface::class, $string_data);
$this->assertEquals('llama', $string_data->getValue());
// Test complex data.
$mapping = $typed_config->get('cat');
/** @var \Drupal\Core\TypedData\ComplexDataInterface $mapping */
$this->assertInstanceOf(ComplexDataInterface::class, $mapping);
$this->assertInstanceOf(StringInterface::class, $mapping->get('type'));
$this->assertEquals('kitten', $mapping->get('type')->getValue());
$this->assertInstanceOf(IntegerInterface::class, $mapping->get('count'));
$this->assertEquals(2, $mapping->get('count')->getValue());
// Verify the item metadata is available.
$this->assertInstanceOf(ComplexDataDefinitionInterface::class, $mapping->getDataDefinition());
$this->assertArrayHasKey('type', $mapping->getProperties());
$this->assertArrayHasKey('count', $mapping->getProperties());
// Test accessing sequences.
$sequence = $typed_config->get('giraffe');
/** @var \Drupal\Core\TypedData\ListInterface $sequence */
$this->assertInstanceOf(SequenceDataDefinition::class, $sequence->getDataDefinition());
$this->assertSame(Sequence::class, $sequence->getDataDefinition()->getClass());
$this->assertSame('sequence', $sequence->getDataDefinition()->getDataType());
$this->assertInstanceOf(ComplexDataInterface::class, $sequence);
$this->assertInstanceOf(StringInterface::class, $sequence->get('hum1'));
$this->assertEquals('hum1', $sequence->get('hum1')->getValue());
$this->assertEquals('hum2', $sequence->get('hum2')->getValue());
$this->assertCount(2, $sequence->getIterator());
// Verify the item metadata is available.
$this->assertInstanceOf(SequenceDataDefinition::class, $sequence->getDataDefinition());
// Test accessing typed config objects for simple config and config
// entities.
$typed_config_manager = \Drupal::service('config.typed');
$typed_config = $typed_config_manager->createFromNameAndData('config_test.validation', \Drupal::configFactory()->get('config_test.validation')->get());
$this->assertInstanceOf(TypedConfigInterface::class, $typed_config);
$this->assertEquals(['_core', 'llama', 'cat', 'giraffe', 'uuid', 'string__not_blank', 'host'], array_keys($typed_config->getElements()));
$this->assertSame('config_test.validation', $typed_config->getName());
$this->assertSame('config_test.validation', $typed_config->getPropertyPath());
$this->assertSame('config_test.validation.llama', $typed_config->get('llama')->getPropertyPath());
$config_test_entity = \Drupal::entityTypeManager()->getStorage('config_test')->create([
'id' => 'test',
'label' => 'Test',
'weight' => 11,
'style' => 'test_style',
]);
$typed_config = $typed_config_manager->createFromNameAndData($config_test_entity->getConfigDependencyName(), $config_test_entity->toArray());
$this->assertInstanceOf(TypedConfigInterface::class, $typed_config);
$this->assertEquals(['uuid', 'langcode', 'status', 'dependencies', 'id', 'label', 'weight', 'style', 'size', 'size_value', 'protected_property'], array_keys($typed_config->getElements()));
}
/**
* Tests the behavior of `NotBlank` on required data.
*
* @testWith ["", false, "This value should not be blank."]
* ["", true, "This value should not be blank."]
* [null, false, "This value should not be blank."]
* [null, true, "This value should not be null."]
*
* @see \Drupal\Core\TypedData\DataDefinition::getConstraints()
* @see \Drupal\Core\TypedData\DataDefinitionInterface::isRequired()
* @see \Drupal\Core\Validation\Plugin\Validation\Constraint\NotNullConstraint
* @see \Symfony\Component\Validator\Constraints\NotBlank::$allowNull
*/
public function testNotBlankInteractionWithNotNull(?string $value, bool $is_required, string $expected_message): void {
\Drupal::configFactory()->getEditable('config_test.validation')
->set('string__not_blank', $value)
->save();
$typed_config = \Drupal::service('config.typed')->get('config_test.validation');
$typed_config->get('string__not_blank')->getDataDefinition()->setRequired($is_required);
$result = $typed_config->validate();
// Expect 1 validation error message: the one from `NotBlank` or `NotNull`.
$this->assertCount(1, $result);
$this->assertSame('string__not_blank', $result->get(0)->getPropertyPath());
$this->assertEquals($expected_message, $result->get(0)->getMessage());
}
/**
* Tests config validation via the Typed Data API.
*/
public function testSimpleConfigValidation(): void {
$config = \Drupal::configFactory()->getEditable('config_test.validation');
/** @var \Drupal\Core\Config\TypedConfigManagerInterface $typed_config_manager */
$typed_config_manager = \Drupal::service('config.typed');
/** @var \Drupal\Core\Config\Schema\TypedConfigInterface $typed_config */
$typed_config = $typed_config_manager->get('config_test.validation');
$result = $typed_config->validate();
$this->assertInstanceOf(ConstraintViolationListInterface::class, $result);
$this->assertEmpty($result);
// Test constraints on primitive types.
$config->set('llama', 'elephant');
$config->save();
$typed_config = $typed_config_manager->get('config_test.validation');
$result = $typed_config->validate();
// Its not a valid llama anymore.
$this->assertCount(1, $result);
$this->assertEquals('no valid llama', $result->get(0)->getMessage());
// Test constraints on mapping.
$config->set('llama', 'llama');
$config->set('cat.type', 'nyans');
$config->save();
$typed_config = $typed_config_manager->get('config_test.validation');
$result = $typed_config->validate();
$this->assertEmpty($result);
// Test constrains on nested mapping.
$config->set('cat.type', 'tiger');
$config->save();
$typed_config = $typed_config_manager->get('config_test.validation');
$result = $typed_config->validate();
$this->assertCount(1, $result);
$this->assertEquals('no valid cat', $result->get(0)->getMessage());
// Test constrains on sequences elements.
$config->set('cat.type', 'nyans');
$config->set('giraffe', ['muh', 'hum2']);
$config->save();
$typed_config = $typed_config_manager->get('config_test.validation');
$result = $typed_config->validate();
$this->assertCount(1, $result);
$this->assertEquals('Giraffes just hum', $result->get(0)->getMessage());
// Test constrains on the sequence itself.
$config->set('giraffe', ['hum', 'hum2', 'invalid-key' => 'hum']);
$config->save();
$typed_config = $typed_config_manager->get('config_test.validation');
$result = $typed_config->validate();
$this->assertCount(1, $result);
$this->assertEquals('giraffe', $result->get(0)->getPropertyPath());
$this->assertEquals('Invalid giraffe key.', $result->get(0)->getMessage());
// Validates mapping.
$typed_config = $typed_config_manager->get('config_test.validation');
$value = $typed_config->getValue();
unset($value['giraffe']);
$value['elephant'] = 'foo';
$value['zebra'] = 'foo';
$typed_config->setValue($value);
$result = $typed_config->validate();
$this->assertCount(3, $result);
// 2 constraint violations triggered by the default validation constraint
// for `type: mapping`
// @see \Drupal\Core\Validation\Plugin\Validation\Constraint\ValidKeysConstraint
$this->assertSame('elephant', $result->get(0)->getPropertyPath());
$this->assertEquals("'elephant' is not a supported key.", $result->get(0)->getMessage());
$this->assertSame('zebra', $result->get(1)->getPropertyPath());
$this->assertEquals("'zebra' is not a supported key.", $result->get(1)->getMessage());
// 1 additional constraint violation triggered by the custom
// constraint for the `config_test.validation` type, which indirectly
// extends `type: mapping` (via `type: config_object`).
// @see \Drupal\config_test\ConfigValidation::validateMapping()
$this->assertEquals('', $result->get(2)->getPropertyPath());
$this->assertEquals('Unexpected keys: elephant, zebra', $result->get(2)->getMessage());
}
}