Initial Drupal 11 with DDEV setup
This commit is contained in:
245
web/core/tests/Drupal/KernelTests/Config/DefaultConfigTest.php
Normal file
245
web/core/tests/Drupal/KernelTests/Config/DefaultConfigTest.php
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
564
web/core/tests/Drupal/KernelTests/Config/Schema/MappingTest.php
Normal file
564
web/core/tests/Drupal/KernelTests/Config/Schema/MappingTest.php
Normal 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,
|
||||
],
|
||||
],
|
||||
]));
|
||||
}
|
||||
|
||||
}
|
||||
231
web/core/tests/Drupal/KernelTests/Config/TypedConfigTest.php
Normal file
231
web/core/tests/Drupal/KernelTests/Config/TypedConfigTest.php
Normal 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());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user