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,95 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests;
use Drupal\Component\Diff\Diff;
/**
* Trait to help with diffing config.
*/
trait AssertConfigTrait {
/**
* Ensures that a specific config diff does not contain unwanted changes.
*
* @param \Drupal\Component\Diff\Diff $result
* The diff result for the passed in config name.
* @param string $config_name
* The config name to check.
* @param array $skipped_config
* An array of skipped config, keyed by string. If the value is TRUE, the
* entire file will be ignored, otherwise it's an array of strings which are
* ignored.
*
* @throws \Exception
* Thrown when a configuration is different.
*/
protected function assertConfigDiff(Diff $result, $config_name, array $skipped_config) {
foreach ($result->getEdits() as $op) {
switch (get_class($op)) {
case 'Drupal\Component\Diff\Engine\DiffOpCopy':
// Nothing to do, a copy is what we expect.
break;
case 'Drupal\Component\Diff\Engine\DiffOpDelete':
case 'Drupal\Component\Diff\Engine\DiffOpChange':
// It is not part of the skipped config, so we can directly throw the
// exception.
if (!in_array($config_name, array_keys($skipped_config))) {
throw new \Exception($config_name . ': ' . var_export($op, TRUE));
}
// Allow to skip entire config files.
if ($skipped_config[$config_name] === TRUE) {
break;
}
// Allow to skip some specific lines of imported config files.
// Ensure that the only changed lines are the ones we marked as
// skipped.
$all_skipped = TRUE;
$changes = get_class($op) == 'Drupal\Component\Diff\Engine\DiffOpDelete' ? $op->orig : $op->closing;
foreach ($changes as $closing) {
// Skip some of the changes, as they are caused by module install
// code.
$found = FALSE;
if (!empty($skipped_config[$config_name])) {
foreach ($skipped_config[$config_name] as $line) {
if (str_contains($closing, $line)) {
$found = TRUE;
break;
}
}
}
$all_skipped = $all_skipped && $found;
}
if (!$all_skipped) {
throw new \Exception($config_name . ': ' . var_export($op, TRUE));
}
break;
case 'Drupal\Component\Diff\Engine\DiffOpAdd':
// The _core property does not exist in the default config.
if ($op->closing[0] === '_core:') {
break;
}
foreach ($op->closing as $closing) {
// The UUIDs don't exist in the default config.
if (str_starts_with($closing, 'uuid: ')) {
break;
}
throw new \Exception($config_name . ': ' . var_export($op, TRUE));
}
break;
default:
throw new \Exception($config_name . ': ' . var_export($op, TRUE));
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Component;
use Drupal\KernelTests\KernelTestBase;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\HttpFoundation\Request;
/**
* Tests the correct rendering of components.
*
* @group sdc
*/
final class ComponentRenderTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system', 'sdc_test'];
/**
* Tests the CSS load order.
*/
public function testCssOrder(): void {
$this->container->get('theme_installer')->install(['sdc_theme_test']);
$build = [
'#type' => 'component',
'#component' => 'sdc_theme_test:css-load-order',
'#props' => [],
];
\Drupal::state()->set('sdc_test_component', $build);
$request = Request::create('/sdc-test-component');
$response = $this->container->get('http_kernel')->handle($request);
$output = $response->getContent();
$crawler = new Crawler($output);
// Assert that both CSS files are attached to the page.
$this->assertNotEmpty($crawler->filter('link[rel="stylesheet"][href*="css-load-order.css"]'));
$this->assertNotEmpty($crawler->filter('link[rel="stylesheet"][href*="css-order-dependent.css"]'));
$all_stylesheets = $crawler->filter('link[rel="stylesheet"]');
$component_position = NULL;
$dependent_position = NULL;
foreach ($all_stylesheets as $index => $stylesheet) {
$href = $stylesheet->attributes->getNamedItem('href')->nodeValue;
if (str_contains($href, 'css-load-order.css')) {
$component_position = $index;
}
if (str_contains($href, 'css-order-dependent.css')) {
$dependent_position = $index;
}
}
// This will assert that css-order-dependent.css is loaded before the
// component's css-load-order.css.
$this->assertGreaterThan($dependent_position, $component_position);
}
}

View File

@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Component\Render;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Url;
use Drupal\KernelTests\KernelTestBase;
/**
* Provides a test covering integration of FormattableMarkup with other systems.
*
* @group Render
*/
class FormattableMarkupKernelTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system'];
/**
* Gets arguments for FormattableMarkup based on Url::fromUri() parameters.
*
* @param string $uri
* The URI of the resource.
* @param array $options
* The options to pass to Url::fromUri().
*
* @return array
* Array containing:
* - ':url': A URL string.
*
* @see \Drupal\Component\Render\FormattableMarkup
*/
protected static function getFormattableMarkupUriArgs($uri, $options = []) {
$args[':url'] = Url::fromUri($uri, $options)->toString();
return $args;
}
/**
* Tests URL ":placeholders" in \Drupal\Component\Render\FormattableMarkup.
*
* @dataProvider providerTestFormattableMarkupUri
*/
public function testFormattableMarkupUri($string, $uri, $options, $expected): void {
$args = self::getFormattableMarkupUriArgs($uri, $options);
$this->assertSame($expected, (string) new FormattableMarkup($string, $args));
}
/**
* @return array
* Data provider for testFormattableMarkupUri().
*/
public static function providerTestFormattableMarkupUri() {
$data = [];
$data['routed-url'] = [
'Hey giraffe <a href=":url">example</a>',
'route:system.admin',
[],
'Hey giraffe <a href="/admin">example</a>',
];
$data['routed-with-query'] = [
'Hey giraffe <a href=":url">example</a>',
'route:system.admin',
['query' => ['bar' => 'baz#']],
'Hey giraffe <a href="/admin?bar=baz%23">example</a>',
];
$data['routed-with-fragment'] = [
'Hey giraffe <a href=":url">example</a>',
'route:system.admin',
['fragment' => 'bar&lt;'],
'Hey giraffe <a href="/admin#bar&amp;lt;">example</a>',
];
$data['unrouted-url'] = [
'Hey giraffe <a href=":url">example</a>',
'base://foo',
[],
'Hey giraffe <a href="/foo">example</a>',
];
$data['unrouted-with-query'] = [
'Hey giraffe <a href=":url">example</a>',
'base://foo',
['query' => ['bar' => 'baz#']],
'Hey giraffe <a href="/foo?bar=baz%23">example</a>',
];
$data['unrouted-with-fragment'] = [
'Hey giraffe <a href=":url">example</a>',
'base://foo',
['fragment' => 'bar&lt;'],
'Hey giraffe <a href="/foo#bar&amp;lt;">example</a>',
];
$data['mailto-protocol'] = [
'Hey giraffe <a href=":url">example</a>',
'mailto:test@example.com',
[],
'Hey giraffe <a href="mailto:test@example.com">example</a>',
];
return $data;
}
/**
* @dataProvider providerTestFormattableMarkupUriWithException
*/
public function testFormattableMarkupUriWithExceptionUri($string, $uri): void {
// Should throw an \InvalidArgumentException, due to Uri::toString().
$this->expectException(\InvalidArgumentException::class);
$args = self::getFormattableMarkupUriArgs($uri);
new FormattableMarkup($string, $args);
}
/**
* @return array
* Data provider for testFormattableMarkupUriWithExceptionUri().
*/
public static function providerTestFormattableMarkupUriWithException() {
$data = [];
$data['js-protocol'] = [
'Hey giraffe <a href=":url">example</a>',
"javascript:alert('xss')",
];
$data['js-with-fromCharCode'] = [
'Hey giraffe <a href=":url">example</a>',
"javascript:alert(String.fromCharCode(88,83,83))",
];
$data['non-url-with-colon'] = [
'Hey giraffe <a href=":url">example</a>',
"llamas: they are not URLs",
];
$data['non-url-with-html'] = [
'Hey giraffe <a href=":url">example</a>',
'<span>not a url</span>',
];
return $data;
}
}

View File

@ -0,0 +1,155 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Components;
use Drupal\Core\Form\FormInterface;
use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormStateInterface;
/**
* Tests the correct rendering of components in form.
*
* @group sdc
*/
class ComponentInFormTest extends ComponentKernelTestBase implements FormInterface {
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'sdc_test',
];
/**
* {@inheritdoc}
*/
protected static $themes = ['sdc_theme_test'];
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'component_in_form_test';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state): array {
$form['normal'] = [
'#type' => 'textfield',
'#title' => 'Normal form element',
'#default_value' => 'fake 1',
];
// We want to test form elements inside a component, itself inside a
// component.
$form['banner'] = [
'#type' => 'component',
'#component' => 'sdc_test:my-banner',
'#props' => [
'ctaText' => 'Click me!',
'ctaHref' => 'https://www.example.org',
'ctaTarget' => '',
],
'banner_body' => [
'#type' => 'component',
'#component' => 'sdc_theme_test:my-card',
'#props' => [
'header' => 'Card header',
],
'card_body' => [
'foo' => [
'#type' => 'textfield',
'#title' => 'Textfield in component',
'#default_value' => 'fake 2',
],
'bar' => [
'#type' => 'select',
'#title' => 'Select in component',
'#options' => [
'option_1' => 'Option 1',
'option_2' => 'Option 2',
],
'#empty_option' => 'Empty option',
'#default_value' => 'option_1',
],
],
],
];
$form['actions'] = [
'#type' => 'actions',
'submit' => [
'#type' => 'submit',
'#value' => 'Submit',
],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state): void {
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
// Check that submitted data are present (set with #default_value).
$data = [
'normal' => 'fake 1',
'foo' => 'fake 2',
'bar' => 'option_1',
];
foreach ($data as $key => $value) {
$this->assertSame($value, $form_state->getValue($key));
}
}
/**
* Tests that fields validation messages are sorted in the fields order.
*/
public function testFormRenderingAndSubmission(): void {
/** @var \Drupal\Core\Form\FormBuilderInterface $form_builder */
$form_builder = \Drupal::service('form_builder');
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$form = $form_builder->getForm($this);
// Test form structure after being processed.
$this->assertTrue($form['normal']['#processed'], 'The normal textfield should have been processed.');
$this->assertTrue($form['banner']['banner_body']['card_body']['bar']['#processed'], 'The textfield inside component should have been processed.');
$this->assertTrue($form['banner']['banner_body']['card_body']['foo']['#processed'], 'The select inside component should have been processed.');
$this->assertTrue($form['actions']['submit']['#processed'], 'The submit button should have been processed.');
// Test form rendering.
$markup = $renderer->renderRoot($form);
$this->setRawContent($markup);
// Ensure form elements are rendered once.
$this->assertCount(1, $this->cssSelect('input[name="normal"]'), 'The normal textfield should have been rendered once.');
$this->assertCount(1, $this->cssSelect('input[name="foo"]'), 'The foo textfield should have been rendered once.');
$this->assertCount(1, $this->cssSelect('select[name="bar"]'), 'The bar select should have been rendered once.');
// Check the position of the form elements in the DOM.
$paths = [
'//form/div[1]/input[@name="normal"]',
'//form/div[2][@data-component-id="sdc_test:my-banner"]/div[2][@class="component--my-banner--body"]/div[1][@data-component-id="sdc_theme_test:my-card"]/div[1][@class="component--my-card__body"]/div[1]/input[@name="foo"]',
'//form/div[2][@data-component-id="sdc_test:my-banner"]/div[2][@class="component--my-banner--body"]/div[1][@data-component-id="sdc_theme_test:my-card"]/div[1][@class="component--my-card__body"]/div[2]/select[@name="bar"]',
];
foreach ($paths as $path) {
$this->assertNotEmpty($this->xpath($path), 'There should be a result with the path: ' . $path . '.');
}
// Test form submission. Assertions are in submitForm().
$form_state = new FormState();
$form_builder->submitForm($this, $form_state);
}
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Components;
use Drupal\Core\Render\Component\Exception\IncompatibleComponentSchema;
/**
* Tests invalid render options for components.
*
* @group sdc
*/
class ComponentInvalidReplacementTest extends ComponentKernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['sdc_test_replacements_invalid'];
/**
* {@inheritdoc}
*/
protected static $themes = ['sdc_theme_test'];
/**
* Ensure that component replacement validates the schema compatibility.
*/
public function testInvalidDefinitionTheme(): void {
$this->expectException(IncompatibleComponentSchema::class);
$this->manager->getDefinitions();
}
}

View File

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Components;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Theme\ComponentNegotiator;
use Drupal\Core\Theme\ComponentPluginManager;
use Drupal\KernelTests\KernelTestBase;
use Symfony\Component\DomCrawler\Crawler;
/**
* Defines a base class for component kernel tests.
*/
abstract class ComponentKernelTestBase extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'user',
'serialization',
];
/**
* Themes to install.
*
* @var string[]
*/
protected static $themes = [];
/**
* The component negotiator.
*
* @return \Drupal\Core\Theme\ComponentNegotiator
*/
protected ComponentNegotiator $negotiator;
/**
* The component plugin manager.
*
* @var \Drupal\Core\Theme\ComponentPluginManager
*/
protected ComponentPluginManager $manager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
if (empty(static::$themes)) {
throw new \Exception('You need to set the protected static $themes property on your test class, with the first item being the default theme.');
}
$this->container->get('theme_installer')->install(static::$themes);
$this->installConfig('system');
$system_theme_config = $this->container->get('config.factory')->getEditable('system.theme');
$theme_name = reset(static::$themes);
$system_theme_config
->set('default', $theme_name)
->save();
$theme_manager = \Drupal::service('theme.manager');
$active_theme = \Drupal::service('theme.initialization')->initTheme($theme_name);
$theme_manager->setActiveTheme($active_theme);
$this->negotiator = new ComponentNegotiator($theme_manager);
$this->manager = \Drupal::service('plugin.manager.sdc');
}
/**
* Renders a component for testing sake.
*
* @param array $component
* Component render array.
* @param \Drupal\Core\Render\BubbleableMetadata|null $metadata
* Bubble metadata.
*
* @return \Symfony\Component\DomCrawler\Crawler
* Crawler for introspecting the rendered component.
*/
protected function renderComponentRenderArray(array $component, ?BubbleableMetadata $metadata = NULL): Crawler {
$component = [
'#type' => 'container',
'#attributes' => [
'id' => 'sdc-wrapper',
],
'component' => $component,
];
$metadata = $metadata ?: new BubbleableMetadata();
$context = new RenderContext();
$renderer = \Drupal::service('renderer');
$output = $renderer->executeInRenderContext($context, fn () => $renderer->render($component));
if (!$context->isEmpty()) {
$metadata->addCacheableDependency($context->pop());
}
return new Crawler((string) $output);
}
}

View File

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Components;
/**
* Tests the component negotiator.
*
* @coversDefaultClass \Drupal\Core\Theme\ComponentNegotiator
* @group sdc
*/
class ComponentNegotiatorTest extends ComponentKernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'sdc_test',
'sdc_test_replacements',
];
/**
* Themes to install.
*
* @var string[]
*/
protected static $themes = [
'sdc_theme_test_enforce_schema', 'sdc_theme_test',
];
/**
* @covers ::negotiate
*/
public function testNegotiate(): void {
$data = [
['sdc_test:my-banner', NULL],
['sdc_theme_test:my-card', 'sdc_theme_test_enforce_schema:my-card'],
[
'sdc_test:my-button',
'sdc_test_replacements:my-button',
],
['invalid:component', NULL],
['invalid^component', NULL],
['', NULL],
];
array_walk($data, function ($test_input) {
[$requested_id, $expected_id] = $test_input;
$negotiated_id = $this->negotiator->negotiate(
$requested_id,
$this->manager->getDefinitions(),
);
$this->assertSame($expected_id, $negotiated_id);
});
}
/**
* Tests rendering components with component replacement.
*/
public function testRenderWithReplacements(): void {
$build = [
'#type' => 'inline_template',
'#template' => "{{ include('sdc_test:my-button') }}",
'#context' => ['text' => 'Like!', 'iconType' => 'like'],
];
$crawler = $this->renderComponentRenderArray($build);
$this->assertNotEmpty($crawler->filter('#sdc-wrapper button[data-component-id="sdc_test_replacements:my-button"]'));
$this->assertNotEmpty($crawler->filter('#sdc-wrapper button .sdc-id:contains("sdc_test_replacements:my-button")'));
// Now test component replacement on themes.
$build = [
'#type' => 'inline_template',
'#template' => "{{ include('sdc_theme_test:my-card') }}",
'#context' => ['header' => 'Foo bar'],
'#variant' => 'horizontal',
];
$crawler = $this->renderComponentRenderArray($build);
$this->assertNotEmpty($crawler->filter('#sdc-wrapper .component--my-card--replaced__body'));
}
}

View File

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Components;
/**
* Tests the node visitor.
*
* @coversDefaultClass \Drupal\Core\Template\ComponentNodeVisitor
* @group sdc
*/
class ComponentNodeVisitorTest extends ComponentKernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system', 'sdc_other_node_visitor'];
/**
* {@inheritdoc}
*/
protected static $themes = ['sdc_theme_test'];
const DEBUG_COMPONENT_ID_PATTERN = '/<!-- ([\n\s\S]*) Component start: ([\SA-Za-z+-:]+) -->/';
const DEBUG_VARIANT_ID_PATTERN = '/<!-- [\n\s\S]* with variant: "([\SA-Za-z+-]+)" -->/';
/**
* Test that other visitors can modify Twig nodes.
*/
public function testOtherVisitorsCanModifyTwigNodes(): void {
$build = [
'#type' => 'inline_template',
'#template' => "{% embed('sdc_theme_test_base:my-card-no-schema') %}{% block card_body %}Foo bar{% endblock %}{% endembed %}",
];
$this->renderComponentRenderArray($build);
// If this is reached, the test passed.
$this->assertTrue(TRUE);
}
/**
* Test debug output for sdc components with component id and variant.
*/
public function testDebugRendersComponentStartWithVariant(): void {
// Enable twig theme debug to ensure that any
// changes to theme debugging format force checking
// that the auto paragraph filter continues to be applied
// correctly.
$twig = \Drupal::service('twig');
$twig->enableDebug();
$build = [
'#type' => 'component',
'#component' => 'sdc_theme_test:my-card',
'#variant' => 'vertical',
'#props' => [
'header' => 'My header',
],
'#slots' => [
'card_body' => 'Foo bar',
],
];
$crawler = $this->renderComponentRenderArray($build);
$content = $crawler->html();
$matches = [];
\preg_match_all(self::DEBUG_COMPONENT_ID_PATTERN, $content, $matches);
$this->assertSame($matches[2][0], 'sdc_theme_test:my-card');
\preg_match_all(self::DEBUG_VARIANT_ID_PATTERN, $content, $matches);
$this->assertSame($matches[1][0], 'vertical');
}
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Components;
/**
* Tests discovery of components in a theme being installed or uninstalled.
*
* @group sdc
*/
class ComponentPluginManagerCachedDiscoveryTest extends ComponentKernelTestBase {
/**
* {@inheritdoc}
*/
protected static $themes = ['stark'];
/**
* Tests cached component plugin discovery on theme install and uninstall.
*/
public function testComponentDiscoveryOnThemeInstall(): void {
// Component in sdc_theme should not be found without sdc_theme installed.
$definitions = \Drupal::service('plugin.manager.sdc')->getDefinitions();
$this->assertArrayNotHasKey('sdc_theme_test:bar', $definitions);
// Component in sdc_theme should be found once sdc_theme is installed.
\Drupal::service('theme_installer')->install(['sdc_theme_test']);
$definitions = \Drupal::service('plugin.manager.sdc')->getDefinitions();
$this->assertArrayHasKey('sdc_theme_test:bar', $definitions);
// Component in sdc_theme should not be found once sdc_theme is uninstalled.
\Drupal::service('theme_installer')->uninstall(['sdc_theme_test']);
$definitions = \Drupal::service('plugin.manager.sdc')->getDefinitions();
$this->assertArrayNotHasKey('sdc_theme_test:bar', $definitions);
}
}

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Components;
use Drupal\Core\Render\Component\Exception\ComponentNotFoundException;
/**
* Tests the component plugin manager.
*
* @group sdc
*/
class ComponentPluginManagerTest extends ComponentKernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system', 'sdc_test', 'sdc_test_replacements'];
/**
* {@inheritdoc}
*/
protected static $themes = ['sdc_theme_test'];
/**
* Test that components render correctly.
*/
public function testFindEmptyMetadataFile(): void {
// Test that empty component metadata files are valid, since there is no
// required property.
$this->assertNotEmpty(
$this->manager->find('sdc_theme_test:bar'),
);
// Test that if the folder name does not match the machine name, the
// component is still available.
$this->assertNotEmpty(
$this->manager->find('sdc_theme_test:foo'),
);
}
/**
* Test that the machine name is grabbed from the *.component.yml.
*
* And not from the enclosing directory.
*/
public function testMismatchingFolderName(): void {
$this->expectException(ComponentNotFoundException::class);
$this->manager->find('sdc_theme_test:mismatching-folder-name');
}
}

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Components;
use Drupal\Core\Render\Component\Exception\InvalidComponentException;
/**
* Tests invalid render options for components.
*
* @group sdc
*/
class ComponentRenderInvalidTest extends ComponentKernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['sdc_test_invalid'];
/**
* {@inheritdoc}
*/
protected static $themes = ['starterkit_theme'];
/**
* Ensure that components in modules without schema fail validation.
*
* The module sdc_test_invalid contains the my-card-no-schema component. This
* component does not have schema definitions.
*/
public function testInvalidDefinitionModule(): void {
$this->expectException(InvalidComponentException::class);
$this->expectExceptionMessage('The component "sdc_test_invalid:my-card-no-schema" does not provide schema information. Schema definitions are mandatory for components declared in modules. For components declared in themes, schema definitions are only mandatory if the "enforce_prop_schemas" key is set to "true" in the theme info file.');
$this->manager->getDefinitions();
}
/**
* Ensure that components in modules without schema fail validation.
*
* The theme sdc_theme_test_enforce_schema_invalid is set as enforcing schemas
* but provides a component without schema.
*/
public function testInvalidDefinitionTheme(): void {
\Drupal::service('theme_installer')->install(['sdc_theme_test_enforce_schema_invalid']);
$active_theme = \Drupal::service('theme.initialization')->initTheme('sdc_theme_test_enforce_schema_invalid');
\Drupal::service('theme.manager')->setActiveTheme($active_theme);
$this->expectException(InvalidComponentException::class);
$this->manager->getDefinitions();
}
}

View File

@ -0,0 +1,384 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Components;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Theme\ComponentPluginManager;
use Drupal\Core\Render\Component\Exception\InvalidComponentDataException;
/**
* Tests the correct rendering of components.
*
* @group sdc
*/
class ComponentRenderTest extends ComponentKernelTestBase {
use StringTranslationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['system', 'sdc_test'];
/**
* {@inheritdoc}
*/
protected static $themes = ['sdc_theme_test'];
/**
* Check using a component with an include and default context.
*/
public function testRenderIncludeDefaultContent(): void {
$build = [
'#type' => 'inline_template',
'#template' => "{% embed('sdc_theme_test_base:my-card-no-schema') %}{% block card_body %}Foo bar{% endblock %}{% endembed %}",
];
$crawler = $this->renderComponentRenderArray($build);
$this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_theme_test_base:my-card-no-schema"] .component--my-card-no-schema__body:contains("Foo bar")'));
}
/**
* Check using a component with an include and no default context.
*
* This covers passing a render array to a 'string' prop, and mapping the
* prop to a context variable.
*/
public function testRenderIncludeDataMapping(): void {
$content = [
'label' => [
'#type' => 'html_tag',
'#tag' => 'span',
'#value' => 'Another button ç',
],
];
$build = [
'#type' => 'inline_template',
'#context' => ['content' => $content],
'#template' => "{{ include('sdc_test:my-button', { text: content.label, iconType: 'external' }, with_context = false) }}",
];
$crawler = $this->renderComponentRenderArray($build);
$this->assertNotEmpty($crawler->filter('#sdc-wrapper button:contains("Another button ç")'));
}
/**
* Render a card with slots that include a CTA component.
*/
public function testRenderEmbedWithNested(): void {
$content = [
'heading' => [
'#type' => 'html_tag',
'#tag' => 'span',
'#value' => 'Just a link',
],
];
$build = [
'#type' => 'inline_template',
'#context' => ['content' => $content],
'#template' => "{% embed 'sdc_theme_test:my-card' with { variant: 'horizontal', header: 'Card header', content: content } only %}{% block card_body %}This is a card with a CTA {{ include('sdc_test:my-cta', { text: content.heading, href: 'https://www.example.org', target: '_blank' }, with_context = false) }}{% endblock %}{% endembed %}",
];
$crawler = $this->renderComponentRenderArray($build);
$this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_theme_test:my-card"] h2.component--my-card__header:contains("Card header")'));
$this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_theme_test:my-card"] .component--my-card__body:contains("This is a card with a CTA")'));
$this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_theme_test:my-card"] .component--my-card__body a[data-component-id="sdc_test:my-cta"]:contains("Just a link")'));
$this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_theme_test:my-card"] .component--my-card__body a[data-component-id="sdc_test:my-cta"][href="https://www.example.org"][target="_blank"]'));
// Now render a component and assert it contains the debug comments.
$build = [
'#type' => 'component',
'#component' => 'sdc_test:my-banner',
'#props' => [
'heading' => 'I am a banner',
'ctaText' => 'Click me',
'ctaHref' => 'https://www.example.org',
'ctaTarget' => '',
],
'#slots' => [
'banner_body' => [
'#type' => 'html_tag',
'#tag' => 'p',
'#value' => 'This is the contents of the banner body.',
],
],
];
$metadata = new BubbleableMetadata();
$this->renderComponentRenderArray($build, $metadata);
$this->assertEquals(['core/components.sdc_test--my-cta', 'core/components.sdc_test--my-banner'], $metadata->getAttachments()['library']);
}
/**
* Check using the libraryOverrides.
*/
public function testRenderLibraryOverrides(): void {
$build = [
'#type' => 'inline_template',
'#template' => "{{ include('sdc_theme_test:lib-overrides') }}",
];
$metadata = new BubbleableMetadata();
$this->renderComponentRenderArray($build, $metadata);
$this->assertEquals(['core/components.sdc_theme_test--lib-overrides'], $metadata->getAttachments()['library']);
}
/**
* Ensures the schema violations are reported properly.
*/
public function testRenderPropValidation(): void {
// 1. Violates the minLength for the text property.
$content = ['label' => '1'];
$build = [
'#type' => 'inline_template',
'#context' => ['content' => $content],
'#template' => "{{ include('sdc_test:my-button', { text: content.label, iconType: 'external' }, with_context = false) }}",
];
try {
$this->renderComponentRenderArray($build);
$this->fail('Invalid prop did not cause an exception');
}
catch (\Throwable) {
$this->addToAssertionCount(1);
}
// 2. Violates the required header property.
$build = [
'#type' => 'inline_template',
'#context' => [],
'#template' => "{{ include('sdc_theme_test:my-card', with_context = false) }}",
];
try {
$this->renderComponentRenderArray($build);
$this->fail('Invalid prop did not cause an exception');
}
catch (\Throwable) {
$this->addToAssertionCount(1);
}
}
/**
* Ensure fuzzy coercing of arrays and objects works properly.
*/
public function testRenderArrayObjectTypeCast(): void {
$content = ['test' => []];
$build = [
'#type' => 'inline_template',
'#context' => ['content' => $content],
'#template' => "{{ include('sdc_test:array-to-object', { testProp: content.test }, with_context = false) }}",
];
try {
$this->renderComponentRenderArray($build);
$this->addToAssertionCount(1);
}
catch (\Throwable) {
$this->fail('Empty array was not converted to object');
}
}
/**
* Ensures that including an invalid component creates an error.
*/
public function testRenderNonExistingComponent(): void {
$build = [
'#type' => 'inline_template',
'#context' => [],
'#template' => "{{ include('sdc_test:INVALID', with_context = false) }}",
];
try {
$this->renderComponentRenderArray($build);
$this->fail('Invalid prop did not cause an exception');
}
catch (\Throwable) {
$this->addToAssertionCount(1);
}
}
/**
* Ensures the attributes are merged properly.
*/
public function testRenderAttributeMerging(): void {
$content = ['label' => 'I am a labels'];
// 1. Check that if it exists Attribute object in the 'attributes' prop, you
// get them merged.
$build = [
'#type' => 'inline_template',
'#context' => [
'content' => $content,
'attributes' => new Attribute(['data-merged-attributes' => 'yes']),
],
'#template' => "{{ include('sdc_test:my-button', { text: content.label, iconType: 'external', attributes: attributes }, with_context = false) }}",
];
$crawler = $this->renderComponentRenderArray($build);
$this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-merged-attributes="yes"][data-component-id="sdc_test:my-button"]'), $crawler->outerHtml());
// 2. Check that if the 'attributes' exists, but there is some other data
// type, then we don't touch it.
$build = [
'#type' => 'inline_template',
'#context' => [
'content' => $content,
'attributes' => 'hard-coded-attr',
],
'#template' => "{{ include('sdc_theme_test_base:my-card-no-schema', { header: content.label, attributes: attributes }, with_context = false) }}",
];
$crawler = $this->renderComponentRenderArray($build);
// The default data attribute should be missing.
$this->assertEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_theme_test_base:my-card-no-schema"]'), $crawler->outerHtml());
$this->assertNotEmpty($crawler->filter('#sdc-wrapper [hard-coded-attr]'), $crawler->outerHtml());
// 3. Check that if the 'attributes' is empty, we get the defaults.
$build = [
'#type' => 'inline_template',
'#context' => ['content' => $content],
'#template' => "{{ include('sdc_theme_test_base:my-card-no-schema', { header: content.label }, with_context = false) }}",
];
$crawler = $this->renderComponentRenderArray($build);
// The default data attribute should not be missing.
$this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_theme_test_base:my-card-no-schema"]'), $crawler->outerHtml());
}
/**
* Ensures the alter callbacks work properly.
*/
public function testRenderElementAlters(): void {
$build = [
'#type' => 'component',
'#component' => 'sdc_test:my-banner',
'#props' => [
'heading' => 'I am a banner',
'ctaText' => 'Click me',
'ctaHref' => 'https://www.example.org',
'ctaTarget' => '',
],
'#propsAlter' => [
fn ($props) => [...$props, 'heading' => 'I am another banner'],
],
'#slots' => [
'banner_body' => [
'#type' => 'html_tag',
'#tag' => 'p',
'#value' => 'This is the contents of the banner body.',
],
],
'#slotsAlter' => [
static fn ($slots) => [...$slots, 'banner_body' => ['#markup' => '<h2>Just something else.</h2>']],
],
];
$crawler = $this->renderComponentRenderArray($build);
$this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_test:my-banner"] .component--my-banner--header h3:contains("I am another banner")'));
$this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_test:my-banner"] .component--my-banner--body:contains("Just something else.")'));
}
/**
* Ensure that the slots allow a render array or a scalar when using the render element.
*/
public function testRenderSlots(): void {
$slots = [
'This is the contents of the banner body.',
[
'#plain_text' => 'This is the contents of the banner body.',
],
];
foreach ($slots as $slot) {
$build = [
'#type' => 'component',
'#component' => 'sdc_test:my-banner',
'#props' => [
'heading' => 'I am a banner',
'ctaText' => 'Click me',
'ctaHref' => 'https://www.example.org',
'ctaTarget' => '',
],
'#slots' => [
'banner_body' => $slot,
],
];
$crawler = $this->renderComponentRenderArray($build);
$this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_test:my-banner"] .component--my-banner--body:contains("This is the contents of the banner body.")'));
}
}
/**
* Ensure that the slots throw an error for invalid slots.
*/
public function testRenderInvalidSlot(): void {
$build = [
'#type' => 'component',
'#component' => 'sdc_test:my-banner',
'#props' => [
'heading' => 'I am a banner',
'ctaText' => 'Click me',
'ctaHref' => 'https://www.example.org',
'ctaTarget' => '',
],
'#slots' => [
'banner_body' => new \stdClass(),
],
];
$this->expectException(InvalidComponentDataException::class);
$this->expectExceptionMessage('Unable to render component "sdc_test:my-banner". A render array or a scalar is expected for the slot "banner_body" when using the render element with the "#slots" property');
$this->renderComponentRenderArray($build);
}
/**
* Ensure that components can have 0 props.
*/
public function testRenderEmptyProps(): void {
$build = [
'#type' => 'component',
'#component' => 'sdc_test:no-props',
'#props' => [],
];
$crawler = $this->renderComponentRenderArray($build);
$this->assertEquals(
$crawler->filter('#sdc-wrapper')->innerText(),
'This is a test string.'
);
}
/**
* Ensure that components variants render.
*/
public function testVariants(): void {
$build = [
'#type' => 'component',
'#component' => 'sdc_test:my-cta',
'#variant' => 'primary',
'#props' => [
'text' => 'Test link',
],
];
$crawler = $this->renderComponentRenderArray($build);
$this->assertNotEmpty($crawler->filter('#sdc-wrapper a[data-component-id="sdc_test:my-cta"][data-component-variant="primary"][class*="my-cta-primary"]'));
// If there were an existing prop named variant, we don't override that for BC reasons.
$build = [
'#type' => 'component',
'#component' => 'sdc_test:my-cta-with-variant-prop',
'#variant' => 'tertiary',
'#props' => [
'text' => 'Test link',
'variant' => 'secondary',
],
];
$crawler = $this->renderComponentRenderArray($build);
$this->assertEmpty($crawler->filter('#sdc-wrapper a[data-component-id="sdc_test:my-cta-with-variant-prop"][data-component-variant="tertiary"]'));
$this->assertNotEmpty($crawler->filter('#sdc-wrapper a[data-component-id="sdc_test:my-cta-with-variant-prop"][data-component-variant="secondary"]'));
}
/**
* Ensures some key aspects of the plugin definition are correctly computed.
*
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function testPluginDefinition(): void {
$plugin_manager = \Drupal::service('plugin.manager.sdc');
assert($plugin_manager instanceof ComponentPluginManager);
$definition = $plugin_manager->getDefinition('sdc_test:my-banner');
$this->assertSame('my-banner', $definition['machineName']);
$this->assertStringEndsWith('system/tests/modules/sdc_test/components/my-banner', $definition['path']);
$this->assertEquals(['core/drupal'], $definition['library']['dependencies']);
$this->assertNotEmpty($definition['library']['css']['component']);
$this->assertSame('my-banner.twig', $definition['template']);
$this->assertNotEmpty($definition['documentation']);
}
}

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());
}
}

View File

@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests;
use Drupal\Core\Form\FormState;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Full generic test suite for any form that data with the configuration system.
*
* @see UserAdminSettingsFormTest
* For a full working implementation.
*/
abstract class ConfigFormTestBase extends KernelTestBase {
use StringTranslationTrait;
/**
* Form ID to use for testing.
*
* @var \Drupal\Core\Form\FormInterface
*/
protected $form;
/**
* Values to use for testing.
*
* @var array
* Contains details for form key, configuration object name, and config key.
* Example:
* @code
* [
* 'user_mail_cancel_confirm_body' => [
* '#value' => $this->randomString(),
* '#config_name' => 'user.mail',
* '#config_key' => 'cancel_confirm.body',
* ],
* ];
* @endcode
*/
protected $values;
/**
* Submit the system_config_form ensure the configuration has expected values.
*/
public function testConfigForm(): void {
// Programmatically submit the given values.
$values = [];
foreach ($this->values as $form_key => $data) {
$values[$form_key] = $data['#value'];
}
$form_state = (new FormState())->setValues($values);
\Drupal::formBuilder()->submitForm($this->form, $form_state);
// Check that the form returns an error when expected, and vice versa.
$errors = $form_state->getErrors();
$valid_form = empty($errors);
$values = print_r($values, TRUE);
$errors = $valid_form ? $this->t('None') : implode(' ', $errors);
$this->assertTrue($valid_form, sprintf('Input values: %s<br/>Validation handler errors: %s', $values, $errors));
foreach ($this->values as $data) {
$this->assertEquals($this->config($data['#config_name'])->get($data['#config_key']), $data['#value']);
}
}
}

View File

@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Action;
use Drupal\Core\Action\Plugin\Action\Derivative\EntityDeleteActionDeriver;
use Drupal\entity_test\Entity\EntityTestMulRevPub;
use Drupal\KernelTests\KernelTestBase;
use Drupal\system\Entity\Action;
use Drupal\user\Entity\User;
/**
* @group Action
*/
class DeleteActionTest extends KernelTestBase {
/**
* The test user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $testUser;
/**
* {@inheritdoc}
*/
protected static $modules = ['system', 'entity_test', 'user'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('entity_test_mulrevpub');
$this->installEntitySchema('user');
$this->testUser = User::create([
'name' => 'foobar',
'mail' => 'foobar@example.com',
]);
$this->testUser->save();
\Drupal::service('current_user')->setAccount($this->testUser);
}
/**
* @covers \Drupal\Core\Action\Plugin\Action\Derivative\EntityDeleteActionDeriver::getDerivativeDefinitions
*/
public function testGetDerivativeDefinitions(): void {
$deriver = new EntityDeleteActionDeriver(\Drupal::entityTypeManager(), \Drupal::translation());
$this->assertEquals([
'entity_test_mulrevpub' => [
'type' => 'entity_test_mulrevpub',
'label' => 'Delete test entity - revisions, data table, and published interface',
'action_label' => 'Delete',
'confirm_form_route_name' => 'entity.entity_test_mulrevpub.delete_multiple_form',
],
'entity_test_revpub' => [
'type' => 'entity_test_revpub',
'label' => 'Delete test entity - revisions and publishing status',
'action_label' => 'Delete',
'confirm_form_route_name' => 'entity.entity_test_revpub.delete_multiple_form',
],
'entity_test_rev' => [
'type' => 'entity_test_rev',
'label' => 'Delete test entity - revisions',
'action_label' => 'Delete',
'confirm_form_route_name' => 'entity.entity_test_rev.delete_multiple_form',
],
], $deriver->getDerivativeDefinitions([
'action_label' => 'Delete',
]));
}
/**
* @covers \Drupal\Core\Action\Plugin\Action\DeleteAction::execute
*/
public function testDeleteAction(): void {
$entity = EntityTestMulRevPub::create(['name' => 'test']);
$entity->save();
$action = Action::create([
'id' => 'entity_delete_action',
'plugin' => 'entity:delete_action:entity_test_mulrevpub',
]);
$action->save();
$action->execute([$entity]);
$this->assertSame(['module' => ['entity_test']], $action->getDependencies());
/** @var \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store */
$temp_store = \Drupal::service('tempstore.private');
$store_entries = $temp_store->get('entity_delete_multiple_confirm')->get($this->testUser->id() . ':entity_test_mulrevpub');
$this->assertSame([$this->testUser->id() => ['en' => 'en']], $store_entries);
}
}

View File

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Action;
use Drupal\Core\Test\AssertMailTrait;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests for the EmailAction plugin.
*
* @group action
*/
class EmailActionTest extends KernelTestBase {
use AssertMailTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['system', 'user', 'dblog'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installSchema('dblog', ['watchdog']);
}
/**
* Tests the email action plugin.
*/
public function testEmailAction(): void {
$this->config('system.site')->set('mail', 'test@example.com')->save();
/** @var \Drupal\Core\Action\ActionManager $plugin_manager */
$plugin_manager = $this->container->get('plugin.manager.action');
$configuration = [
'recipient' => 'test@example.com',
'subject' => 'Test subject',
'message' => 'Test message',
];
$plugin_manager
->createInstance('action_send_email_action', $configuration)
->execute();
$mails = $this->getMails();
$this->assertCount(1, $this->getMails());
$this->assertEquals('test@example.com', $mails[0]['to']);
$this->assertEquals('Test subject', $mails[0]['subject']);
$this->assertEquals("Test message\n", $mails[0]['body']);
// Ensure that the email sending is logged.
$log = \Drupal::database()
->select('watchdog', 'w')
->fields('w', ['message', 'variables'])
->orderBy('wid', 'DESC')
->range(0, 1)
->execute()
->fetch();
$this->assertEquals('Sent email to %recipient', $log->message);
$variables = unserialize($log->variables);
$this->assertEquals('test@example.com', $variables['%recipient']);
}
}

View File

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Action;
use Drupal\Core\Action\Plugin\Action\Derivative\EntityPublishedActionDeriver;
use Drupal\entity_test\Entity\EntityTestMulRevPub;
use Drupal\KernelTests\KernelTestBase;
use Drupal\system\Entity\Action;
/**
* @group Action
*/
class PublishActionTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system', 'entity_test', 'user'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('entity_test_mulrevpub');
}
/**
* @covers \Drupal\Core\Action\Plugin\Action\Derivative\EntityPublishedActionDeriver::getDerivativeDefinitions
*/
public function testGetDerivativeDefinitions(): void {
$deriver = new EntityPublishedActionDeriver(\Drupal::entityTypeManager(), \Drupal::translation());
$definitions = $deriver->getDerivativeDefinitions([
'action_label' => 'Save',
]);
$this->assertEquals([
'type' => 'entity_test_mulrevpub',
'label' => 'Save test entity - revisions, data table, and published interface',
'action_label' => 'Save',
], $definitions['entity_test_mulrevpub']);
}
/**
* @covers \Drupal\Core\Action\Plugin\Action\PublishAction::execute
*/
public function testPublishAction(): void {
$entity = EntityTestMulRevPub::create(['name' => 'test']);
$entity->setUnpublished()->save();
$action = Action::create([
'id' => 'entity_publish_action',
'plugin' => 'entity:publish_action:entity_test_mulrevpub',
]);
$action->save();
$this->assertFalse($entity->isPublished());
$action->execute([$entity]);
$this->assertTrue($entity->isPublished());
$this->assertSame(['module' => ['entity_test']], $action->getDependencies());
}
/**
* @covers \Drupal\Core\Action\Plugin\Action\UnpublishAction::execute
*/
public function testUnpublishAction(): void {
$entity = EntityTestMulRevPub::create(['name' => 'test']);
$entity->setPublished()->save();
$action = Action::create([
'id' => 'entity_unpublish_action',
'plugin' => 'entity:unpublish_action:entity_test_mulrevpub',
]);
$action->save();
$this->assertTrue($entity->isPublished());
$action->execute([$entity]);
$this->assertFalse($entity->isPublished());
$this->assertSame(['module' => ['entity_test']], $action->getDependencies());
}
}

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Action;
use Drupal\Core\Action\Plugin\Action\Derivative\EntityChangedActionDeriver;
use Drupal\entity_test\Entity\EntityTestMulChanged;
use Drupal\KernelTests\KernelTestBase;
use Drupal\system\Entity\Action;
/**
* @group Action
*/
class SaveActionTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system', 'entity_test', 'user'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('entity_test_mul_changed');
}
/**
* @covers \Drupal\Core\Action\Plugin\Action\Derivative\EntityChangedActionDeriver::getDerivativeDefinitions
*/
public function testGetDerivativeDefinitions(): void {
$deriver = new EntityChangedActionDeriver(\Drupal::entityTypeManager(), \Drupal::translation());
$definitions = $deriver->getDerivativeDefinitions([
'action_label' => 'Save',
]);
$this->assertEquals([
'type' => 'entity_test_mul_changed',
'label' => 'Save test entity - multiple changed and data table',
'action_label' => 'Save',
], $definitions['entity_test_mul_changed']);
}
/**
* @covers \Drupal\Core\Action\Plugin\Action\SaveAction::execute
*/
public function testSaveAction(): void {
$entity = EntityTestMulChanged::create(['name' => 'test']);
$entity->save();
$saved_time = $entity->getChangedTime();
$action = Action::create([
'id' => 'entity_save_action',
'plugin' => 'entity:save_action:entity_test_mul_changed',
]);
$action->save();
$action->execute([$entity]);
$this->assertNotSame($saved_time, $entity->getChangedTime());
$this->assertSame(['module' => ['entity_test']], $action->getDependencies());
}
}

View File

@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Ajax;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\InsertCommand;
use Drupal\Core\EventSubscriber\AjaxResponseSubscriber;
use Drupal\KernelTests\KernelTestBase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* Performs tests on AJAX framework commands.
*
* @group Ajax
*/
class CommandsTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'node',
'ajax_test',
'ajax_forms_test',
];
/**
* Regression test: Settings command exists regardless of JS aggregation.
*/
public function testAttachedSettings(): void {
$assert = function ($message) {
$response = new AjaxResponse();
$response->setAttachments([
'library' => ['core/drupalSettings'],
'drupalSettings' => ['foo' => 'bar'],
]);
$ajax_response_attachments_processor = \Drupal::service('ajax_response.attachments_processor');
$subscriber = new AjaxResponseSubscriber(fn() => $ajax_response_attachments_processor);
$event = new ResponseEvent(
\Drupal::service('http_kernel'),
new Request(),
HttpKernelInterface::MAIN_REQUEST,
$response
);
$subscriber->onResponse($event);
$expected = [
'command' => 'settings',
];
$this->assertCommand($response->getCommands(), $expected, $message);
};
$config = $this->config('system.performance');
$config->set('js.preprocess', FALSE)->save();
$assert('Settings command exists when JS aggregation is disabled.');
$config->set('js.preprocess', TRUE)->save();
$assert('Settings command exists when JS aggregation is enabled.');
}
/**
* Checks empty content in commands does not throw exceptions.
*
* @doesNotPerformAssertions
*/
public function testEmptyInsertCommand(): void {
(new InsertCommand('foobar', []))->render();
}
/**
* Asserts the array of Ajax commands contains the searched command.
*
* An AjaxResponse object stores an array of Ajax commands. This array
* sometimes includes commands automatically provided by the framework in
* addition to commands returned by a particular controller. During testing,
* we're usually interested that a particular command is present, and don't
* care whether other commands precede or follow the one we're interested in.
* Additionally, the command we're interested in may include additional data
* that we're not interested in. Therefore, this function simply asserts that
* one of the commands in $haystack contains all of the keys and values in
* $needle. Furthermore, if $needle contains a 'settings' key with an array
* value, we simply assert that all keys and values within that array are
* present in the command we're checking, and do not consider it a failure if
* the actual command contains additional settings that aren't part of
* $needle.
*
* @param array $haystack
* An array of rendered Ajax commands returned by the server.
* @param array $needle
* Array of info we're expecting in one of those commands.
* @param string $message
* An assertion message.
*
* @internal
*/
protected function assertCommand(array $haystack, array $needle, string $message): void {
$found = FALSE;
foreach ($haystack as $command) {
// If the command has additional settings that we're not testing for, do
// not consider that a failure.
if (isset($command['settings']) && is_array($command['settings']) && isset($needle['settings']) && is_array($needle['settings'])) {
$command['settings'] = array_intersect_key($command['settings'], $needle['settings']);
}
// If the command has additional data that we're not testing for, do not
// consider that a failure. Also, == instead of ===, because we don't
// require the key/value pairs to be in any particular order
// (http://php.net/manual/language.operators.array.php).
if (array_intersect_key($command, $needle) == $needle) {
$found = TRUE;
break;
}
}
$this->assertTrue($found, $message);
}
}

View File

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Archiver;
use Drupal\KernelTests\Core\File\FileTestBase;
use Drupal\Tests\TestFileCreationTrait;
/**
* Provides archive specific assertions and helper properties for archive tests.
*/
abstract class ArchiverTestBase extends FileTestBase {
use TestFileCreationTrait;
/**
* The archiver plugin identifier.
*
* @var string
*/
protected $archiverPluginId;
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->fileSystem = $this->container->get('file_system');
}
/**
* Asserts an archive contains a given file.
*
* @param string $path
* Absolute file path to an archived file.
* @param string $file
* File to assert does exist within the archived file.
* @param array $configuration
* Optional configuration to pass to the archiver plugin.
*/
protected function assertArchiveContainsFile($path, $file, array $configuration = []) {
$configuration['filepath'] = $path;
/** @var \Drupal\Core\Archiver\ArchiverManager $manager */
$manager = $this->container->get('plugin.manager.archiver');
$archive = $manager->createInstance($this->archiverPluginId, $configuration);
$this->assertContains($file, $archive->listContents(), sprintf('The "%s" archive contains the "%s" file.', $path, $file));
}
/**
* Asserts an archive does not contain a given file.
*
* @param string $path
* Absolute file path to an archived file.
* @param string $file
* File to assert does not exist within the archived file.
* @param array $configuration
* Optional configuration to pass to the archiver plugin.
*/
protected function assertArchiveNotContainsFile($path, $file, array $configuration = []) {
$configuration['filepath'] = $path;
/** @var \Drupal\Core\Archiver\ArchiverManager $manager */
$manager = $this->container->get('plugin.manager.archiver');
$archive = $manager->createInstance($this->archiverPluginId, $configuration);
$this->assertNotContains($file, $archive->listContents(), sprintf('The "%s" archive does not contain the "%s" file.', $path, $file));
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Archiver;
use Drupal\Core\Archiver\Tar;
/**
* @coversDefaultClass \Drupal\Core\Archiver\Tar
* @group tar
*/
class TarTest extends ArchiverTestBase {
/**
* {@inheritdoc}
*/
protected $archiverPluginId = 'Tar';
/**
* Tests that the Tar archive is created if it does not exist.
*/
public function testCreateArchive(): void {
$textFile = current($this->getTestFiles('text'));
$archiveFilename = $this->fileSystem->realpath('public://' . $this->randomMachineName() . '.tar');
$tar = new Tar($archiveFilename);
$tar->add($this->fileSystem->realPath($textFile->uri));
$this->assertFileExists($archiveFilename, 'Archive is automatically created if the file does not exist.');
$this->assertArchiveContainsFile($archiveFilename, $this->fileSystem->realPath($textFile->uri));
}
}

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Archiver;
use Drupal\Core\Archiver\Zip;
/**
* @coversDefaultClass \Drupal\Core\Archiver\Zip
* @group zip
*/
class ZipTest extends ArchiverTestBase {
/**
* {@inheritdoc}
*/
protected $archiverPluginId = 'Zip';
/**
* Tests that the Zip archive is created if it does not exist.
*/
public function testCreateArchive(): void {
$textFile = current($this->getTestFiles('text'));
$archiveFilename = $this->fileSystem->realpath('public://' . $this->randomMachineName() . '.zip');
$zip = new Zip($archiveFilename, [
'flags' => \ZipArchive::CREATE,
]);
$zip->add($this->fileSystem->realPath($textFile->uri));
// Close the archive and make sure it is written to disk.
$this->assertTrue($zip->getArchive()->close(), 'Successfully closed archive.');
$this->assertFileExists($archiveFilename, 'Archive is automatically created if the file does not exist.');
$this->assertArchiveContainsFile($archiveFilename, $this->fileSystem->realPath($textFile->uri));
}
/**
* Tests that the Zip archiver is created and overwritten.
*/
public function testOverwriteArchive(): void {
// Create an archive similarly to how it's done in ::testCreateArchive.
$files = $this->getTestFiles('text');
$textFile = current($files);
$archiveFilename = $this->fileSystem->realpath('public://' . $this->randomMachineName() . '.zip');
$zip = new Zip($archiveFilename, [
'flags' => \ZipArchive::CREATE,
]);
$zip->add($this->fileSystem->realPath($textFile->uri));
$zip->getArchive()->close();
$this->assertArchiveContainsFile($archiveFilename, $this->fileSystem->realPath($textFile->uri));
// Overwrite the zip with just a new text file.
$secondTextFile = next($files);
$zip = new Zip($archiveFilename, [
'flags' => \ZipArchive::OVERWRITE,
]);
$zip->add($this->fileSystem->realpath($secondTextFile->uri));
$zip->getArchive()->close();
$this->assertArchiveNotContainsFile($archiveFilename, $this->fileSystem->realPath($textFile->uri));
$this->assertArchiveContainsFile($archiveFilename, $this->fileSystem->realPath($secondTextFile->uri));
}
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Asset;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Asset\AssetQueryString;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the asset query string functionality.
*
* @group Asset
* @coversDefaultClass \Drupal\Core\Asset\AssetQueryString
*/
class AssetQueryStringTest extends KernelTestBase {
/**
* @covers ::get
* @covers ::reset
*/
public function testResetGet(): void {
$state = $this->container->get('state');
// Return a fixed timestamp.
$time = $this->createStub(TimeInterface::class);
$time->method('getRequestTime')
->willReturn(1683246590);
$queryString = new AssetQueryString($state, $time);
$queryString->reset();
$value = $queryString->get();
$this->assertEquals('ru5tdq', $value);
}
}

View File

@ -0,0 +1,492 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Asset;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Asset\AttachedAssets;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests #attached assets: attached asset libraries and JavaScript settings.
*
* I.e. tests:
*
* @code
* $build['#attached']['library'] = …
* $build['#attached']['drupalSettings'] = …
* @endcode
*
* @group Common
* @group Asset
*/
class AttachedAssetsTest extends KernelTestBase {
/**
* The asset resolver service.
*
* @var \Drupal\Core\Asset\AssetResolverInterface
*/
protected $assetResolver;
/**
* The renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The file URL generator.
*
* @var \Drupal\Core\File\FileUrlGeneratorInterface
*/
protected $fileUrlGenerator;
/**
* {@inheritdoc}
*/
protected static $modules = ['language', 'common_test', 'system'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->assetResolver = $this->container->get('asset.resolver');
$this->renderer = $this->container->get('renderer');
$this->fileUrlGenerator = $this->container->get('file_url_generator');
}
/**
* Tests that default CSS and JavaScript is empty.
*/
public function testDefault(): void {
$assets = new AttachedAssets();
$this->assertEquals([], $this->assetResolver->getCssAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage()), 'Default CSS is empty.');
[$js_assets_header, $js_assets_footer] = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage());
$this->assertEquals([], $js_assets_header, 'Default header JavaScript is empty.');
$this->assertEquals([], $js_assets_footer, 'Default footer JavaScript is empty.');
}
/**
* Tests non-existing libraries.
*/
public function testLibraryUnknown(): void {
$build['#attached']['library'][] = 'core/unknown';
$assets = AttachedAssets::createFromRenderArray($build);
$this->assertSame([], $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[0], 'Unknown library was not added to the page.');
}
/**
* Tests adding a CSS and a JavaScript file.
*/
public function testAddFiles(): void {
$build['#attached']['library'][] = 'common_test/files';
$assets = AttachedAssets::createFromRenderArray($build);
$css = $this->assetResolver->getCssAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage());
$js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1];
$this->assertArrayHasKey('core/modules/system/tests/modules/common_test/bar.css', $css);
$this->assertArrayHasKey('core/modules/system/tests/modules/common_test/foo.js', $js);
$css_render_array = \Drupal::service('asset.css.collection_renderer')->render($css);
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_css = (string) $this->renderer->renderInIsolation($css_render_array);
$rendered_js = (string) $this->renderer->renderInIsolation($js_render_array);
$query_string = $this->container->get('asset.query_string')->get();
$this->assertStringContainsString('<link rel="stylesheet" media="all" href="' . $this->fileUrlGenerator->generateString('core/modules/system/tests/modules/common_test/bar.css') . '?' . $query_string . '" />', $rendered_css, 'Rendering an external CSS file.');
$this->assertStringContainsString('<script src="' . $this->fileUrlGenerator->generateString('core/modules/system/tests/modules/common_test/foo.js') . '?' . $query_string . '"></script>', $rendered_js, 'Rendering an external JavaScript file.');
}
/**
* Tests adding JavaScript settings.
*/
public function testAddJsSettings(): void {
// Add a file in order to test default settings.
$build['#attached']['library'][] = 'core/drupalSettings';
$assets = AttachedAssets::createFromRenderArray($build);
$this->assertEquals([], $assets->getSettings(), 'JavaScript settings on $assets are empty.');
$javascript = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1];
$this->assertArrayHasKey('currentPath', $javascript['drupalSettings']['data']['path']);
$this->assertArrayHasKey('currentPath', $assets->getSettings()['path']);
$assets->setSettings(['drupal' => 'rocks', 'dries' => 280342800]);
$javascript = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1];
$this->assertEquals(280342800, $javascript['drupalSettings']['data']['dries'], 'JavaScript setting is set correctly.');
$this->assertEquals('rocks', $javascript['drupalSettings']['data']['drupal'], 'The other JavaScript setting is set correctly.');
}
/**
* Tests adding external CSS and JavaScript files.
*/
public function testAddExternalFiles(): void {
$build['#attached']['library'][] = 'common_test/external';
$assets = AttachedAssets::createFromRenderArray($build);
$css = $this->assetResolver->getCssAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage());
$js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1];
$this->assertArrayHasKey('http://example.com/stylesheet.css', $css);
$this->assertArrayHasKey('http://example.com/script.js', $js);
$css_render_array = \Drupal::service('asset.css.collection_renderer')->render($css);
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_css = (string) $this->renderer->renderInIsolation($css_render_array);
$rendered_js = (string) $this->renderer->renderInIsolation($js_render_array);
$this->assertStringContainsString('<link rel="stylesheet" media="all" href="http://example.com/stylesheet.css" />', $rendered_css, 'Rendering an external CSS file.');
$this->assertStringContainsString('<script src="http://example.com/script.js"></script>', $rendered_js, 'Rendering an external JavaScript file.');
}
/**
* Tests adding JavaScript files with additional attributes.
*/
public function testAttributes(): void {
$build['#attached']['library'][] = 'common_test/js-attributes';
$assets = AttachedAssets::createFromRenderArray($build);
$js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1];
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_js = (string) $this->renderer->renderInIsolation($js_render_array);
$expected_1 = '<script src="http://example.com/deferred-external.js" foo="bar" defer></script>';
$expected_2 = '<script src="' . $this->fileUrlGenerator->generateString('core/modules/system/tests/modules/common_test/deferred-internal.js') . '?v=1" defer bar="foo"></script>';
$this->assertStringContainsString($expected_1, $rendered_js, 'Rendered external JavaScript with correct defer and random attributes.');
$this->assertStringContainsString($expected_2, $rendered_js, 'Rendered internal JavaScript with correct defer and random attributes.');
}
/**
* Tests that attributes are maintained when JS aggregation is enabled.
*/
public function testAggregatedAttributes(): void {
$build['#attached']['library'][] = 'common_test/js-attributes';
$assets = AttachedAssets::createFromRenderArray($build);
$js = $this->assetResolver->getJsAssets($assets, TRUE, \Drupal::languageManager()->getCurrentLanguage())[1];
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_js = (string) $this->renderer->renderInIsolation($js_render_array);
$expected_1 = '<script src="http://example.com/deferred-external.js" foo="bar" defer></script>';
$expected_2 = '<script src="' . $this->fileUrlGenerator->generateString('core/modules/system/tests/modules/common_test/deferred-internal.js') . '?v=1" defer bar="foo"></script>';
$this->assertStringContainsString($expected_1, $rendered_js, 'Rendered external JavaScript with correct defer and random attributes.');
$this->assertStringContainsString($expected_2, $rendered_js, 'Rendered internal JavaScript with correct defer and random attributes.');
}
/**
* Integration test for CSS/JS aggregation.
*/
public function testAggregation(): void {
$build['#attached']['library'][] = 'core/drupal.timezone';
$build['#attached']['library'][] = 'core/drupal.vertical-tabs';
$assets = AttachedAssets::createFromRenderArray($build);
$this->assertCount(1, $this->assetResolver->getCssAssets($assets, TRUE, \Drupal::languageManager()->getCurrentLanguage()), 'There is a sole aggregated CSS asset.');
[$header_js, $footer_js] = $this->assetResolver->getJsAssets($assets, TRUE, \Drupal::languageManager()->getCurrentLanguage());
$this->assertEquals([], \Drupal::service('asset.js.collection_renderer')->render($header_js), 'There are 0 JavaScript assets in the header.');
$rendered_footer_js = \Drupal::service('asset.js.collection_renderer')->render($footer_js);
$this->assertCount(3, $rendered_footer_js, 'There are 3 JavaScript assets in the footer.');
$this->assertEquals('drupal-settings-json', $rendered_footer_js[0]['#attributes']['data-drupal-selector'], 'The first of the two JavaScript assets in the footer has drupal settings.');
$this->assertStringContainsString('jquery.min.js', $rendered_footer_js[1]['#attributes']['src'], 'The second of the two JavaScript assets in the footer is jquery.min.js.');
$this->assertStringStartsWith(base_path(), $rendered_footer_js[2]['#attributes']['src'], 'The third of the two JavaScript assets in the footer has the sole aggregated JavaScript asset.');
}
/**
* Tests JavaScript settings.
*/
public function testSettings(): void {
$build = [];
$build['#attached']['library'][] = 'core/drupalSettings';
// Nonsensical value to verify if it's possible to override path settings.
$build['#attached']['drupalSettings']['path']['pathPrefix'] = 'does_not_exist';
$assets = AttachedAssets::createFromRenderArray($build);
$js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1];
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
// Cast to string since this returns a \Drupal\Core\Render\Markup object.
$rendered_js = (string) $this->renderer->renderInIsolation($js_render_array);
// Parse the generated drupalSettings <script> back to a PHP representation.
$startToken = '{';
$endToken = '}';
$start = strpos($rendered_js, $startToken);
$end = strrpos($rendered_js, $endToken);
// Convert to a string, as $renderer_js is a \Drupal\Core\Render\Markup
// object.
$json = mb_substr($rendered_js, $start, $end - $start + 1);
$parsed_settings = Json::decode($json);
// Test whether the settings for core/drupalSettings are available.
$this->assertTrue(isset($parsed_settings['path']['baseUrl']), 'drupalSettings.path.baseUrl is present.');
$this->assertSame('does_not_exist', $parsed_settings['path']['pathPrefix'], 'drupalSettings.path.pathPrefix is present and has the correct (overridden) value.');
$this->assertSame('', $parsed_settings['path']['currentPath'], 'drupalSettings.path.currentPath is present and has the correct value.');
$this->assertFalse($parsed_settings['path']['currentPathIsAdmin'], 'drupalSettings.path.currentPathIsAdmin is present and has the correct value.');
$this->assertFalse($parsed_settings['path']['isFront'], 'drupalSettings.path.isFront is present and has the correct value.');
$this->assertSame('en', $parsed_settings['path']['currentLanguage'], 'drupalSettings.path.currentLanguage is present and has the correct value.');
// Tests whether altering JavaScript settings via hook_js_settings_alter()
// is working as expected.
// @see common_test_js_settings_alter()
$this->assertSame('☃', $parsed_settings['pluralDelimiter']);
$this->assertSame('bar', $parsed_settings['foo']);
}
/**
* Tests JS assets depending on the 'core/<head>' virtual library.
*/
public function testHeaderHTML(): void {
$build['#attached']['library'][] = 'common_test/js-header';
$assets = AttachedAssets::createFromRenderArray($build);
$js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[0];
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_js = (string) $this->renderer->renderInIsolation($js_render_array);
$query_string = $this->container->get('asset.query_string')->get();
$this->assertStringContainsString('<script src="' . $this->fileUrlGenerator->generateString('core/modules/system/tests/modules/common_test/header.js') . '?' . $query_string . '"></script>', $rendered_js, 'The JS asset in common_test/js-header appears in the header.');
$this->assertStringContainsString('<script src="' . $this->fileUrlGenerator->generateString('core/misc/drupal.js'), $rendered_js, 'The JS asset of the direct dependency (core/drupal) of common_test/js-header appears in the header.');
$this->assertStringContainsString('<script src="' . $this->fileUrlGenerator->generateString('core/misc/drupalSettingsLoader.js'), $rendered_js, 'The JS asset of the indirect dependency (core/drupalSettings) of common_test/js-header appears in the header.');
}
/**
* Tests that for assets with cache = FALSE, Drupal sets preprocess = FALSE.
*/
public function testNoCache(): void {
$build['#attached']['library'][] = 'common_test/no-cache';
$assets = AttachedAssets::createFromRenderArray($build);
$js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1];
$this->assertFalse($js['core/modules/system/tests/modules/common_test/no_cache.js']['preprocess'], 'Setting cache to FALSE sets preprocess to FALSE when adding JavaScript.');
}
/**
* Tests JavaScript versioning.
*/
public function testVersionQueryString(): void {
$build['#attached']['library'][] = 'core/once';
$assets = AttachedAssets::createFromRenderArray($build);
$js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1];
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_js = (string) $this->renderer->renderInIsolation($js_render_array);
$this->assertStringContainsString('core/assets/vendor/once/once.min.js?v=1.0.1', $rendered_js, 'JavaScript version identifiers correctly appended to URLs');
}
/**
* Tests JavaScript and CSS asset ordering.
*/
public function testRenderOrder(): void {
$build['#attached']['library'][] = 'common_test/order';
$assets = AttachedAssets::createFromRenderArray($build);
// Construct the expected result from the regex.
$expected_order_js = [
"-8_1",
"-8_2",
"-8_3",
"-8_4",
// The external script.
"-5_1",
"-3_1",
"-3_2",
"0_1",
"0_2",
"0_3",
];
// Retrieve the rendered JavaScript and test against the regex.
$js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1];
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_js = (string) $this->renderer->renderInIsolation($js_render_array);
$matches = [];
if (preg_match_all('/weight_([-0-9]+_[0-9]+)/', $rendered_js, $matches)) {
$result = $matches[1];
}
else {
$result = [];
}
$this->assertSame($expected_order_js, $result, 'JavaScript is added in the expected weight order.');
// Construct the expected result from the regex.
$expected_order_css = [
// Base.
'base_weight_-101_1',
'base_weight_-8_1',
'layout_weight_-101_1',
'base_weight_0_1',
'base_weight_0_2',
// Layout.
'layout_weight_-8_1',
'component_weight_-101_1',
'layout_weight_0_1',
'layout_weight_0_2',
// Component.
'component_weight_-8_1',
'state_weight_-101_1',
'component_weight_0_1',
'component_weight_0_2',
// State.
'state_weight_-8_1',
'theme_weight_-101_1',
'state_weight_0_1',
'state_weight_0_2',
// Theme.
'theme_weight_-8_1',
'theme_weight_0_1',
'theme_weight_0_2',
];
// Retrieve the rendered CSS and test against the regex.
$css = $this->assetResolver->getCssAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage());
$css_render_array = \Drupal::service('asset.css.collection_renderer')->render($css);
$rendered_css = (string) $this->renderer->renderInIsolation($css_render_array);
$matches = [];
if (preg_match_all('/([a-z]+)_weight_([-0-9]+_[0-9]+)/', $rendered_css, $matches)) {
$result = $matches[0];
}
else {
$result = [];
}
$this->assertSame($expected_order_css, $result, 'CSS is added in the expected weight order.');
}
/**
* Tests rendering the JavaScript with a file's weight above jQuery's.
*/
public function testRenderDifferentWeight(): void {
// If a library contains assets A and B, and A is listed first, then B can
// still make itself appear first by defining a lower weight.
$build['#attached']['library'][] = 'core/jquery';
$build['#attached']['library'][] = 'common_test/weight';
$assets = AttachedAssets::createFromRenderArray($build);
$js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1];
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_js = (string) $this->renderer->renderInIsolation($js_render_array);
// Verify that lighter CSS assets are rendered first.
$this->assertLessThan(strpos($rendered_js, 'first.js'), strpos($rendered_js, 'lighter.css'));
// Verify that lighter JavaScript assets are rendered first.
$this->assertLessThan(strpos($rendered_js, 'first.js'), strpos($rendered_js, 'lighter.js'));
// Verify that a JavaScript file is rendered before jQuery.
$this->assertLessThan(strpos($rendered_js, 'core/assets/vendor/jquery/jquery.min.js'), strpos($rendered_js, 'before-jquery.js'));
}
/**
* Tests altering a JavaScript's weight via hook_js_alter().
*
* @see common_test_js_alter()
*/
public function testAlter(): void {
// Add both tableselect.js and alter.js.
$build['#attached']['library'][] = 'core/drupal.tableselect';
$build['#attached']['library'][] = 'common_test/hook_js_alter';
$assets = AttachedAssets::createFromRenderArray($build);
// Render the JavaScript, testing if alter.js was altered to be before
// tableselect.js. See common_test_js_alter() to see where this alteration
// takes place.
$js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1];
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_js = (string) $this->renderer->renderInIsolation($js_render_array);
// Verify that JavaScript weight is correctly altered by the alter hook.
$this->assertLessThan(strpos($rendered_js, 'core/misc/tableselect.js'), strpos($rendered_js, 'alter.js'));
}
/**
* Adds a JavaScript library to the page and alters it.
*
* @see common_test_library_info_alter()
*/
public function testLibraryAlter(): void {
// Verify that common_test altered the title of loadjs.
/** @var \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery */
$library_discovery = \Drupal::service('library.discovery');
$library = $library_discovery->getLibraryByName('core', 'loadjs');
$this->assertEquals('0.0', $library['version'], 'Registered libraries were altered.');
// common_test_library_info_alter() also added a dependency on jQuery Form.
$build['#attached']['library'][] = 'core/loadjs';
$assets = AttachedAssets::createFromRenderArray($build);
$js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1];
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_js = (string) $this->renderer->renderInIsolation($js_render_array);
$this->assertStringContainsString('core/misc/jquery.form.js', (string) $rendered_js, 'Altered library dependencies are added to the page.');
}
/**
* Dynamically defines an asset library and alters it.
*/
public function testDynamicLibrary(): void {
/** @var \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery */
$library_discovery = \Drupal::service('library.discovery');
// Retrieve a dynamic library definition.
// @see common_test_library_info_build()
\Drupal::state()->set('common_test.library_info_build_test', TRUE);
$library_discovery->clear();
$dynamic_library = $library_discovery->getLibraryByName('common_test', 'dynamic_library');
$this->assertIsArray($dynamic_library);
$this->assertArrayHasKey('version', $dynamic_library);
$this->assertSame('1.0', $dynamic_library['version']);
// Make sure the dynamic library definition could be altered.
// @see common_test_library_info_alter()
$this->assertArrayHasKey('dependencies', $dynamic_library);
$this->assertSame(['core/jquery'], $dynamic_library['dependencies']);
}
/**
* Tests that multiple modules can implement libraries with the same name.
*
* @see common_test.library.yml
*/
public function testLibraryNameConflicts(): void {
/** @var \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery */
$library_discovery = \Drupal::service('library.discovery');
$loadjs = $library_discovery->getLibraryByName('common_test', 'loadjs');
$this->assertEquals('0.1', $loadjs['version'], 'Alternative libraries can be added to the page.');
}
/**
* Tests JavaScript files that have query strings attached get added right.
*/
public function testAddJsFileWithQueryString(): void {
$build['#attached']['library'][] = 'common_test/querystring';
$assets = AttachedAssets::createFromRenderArray($build);
$css = $this->assetResolver->getCssAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage());
$js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1];
$this->assertArrayHasKey('core/modules/system/tests/modules/common_test/querystring.css?arg1=value1&arg2=value2', $css);
$this->assertArrayHasKey('core/modules/system/tests/modules/common_test/querystring.js?arg1=value1&arg2=value2', $js);
$css_render_array = \Drupal::service('asset.css.collection_renderer')->render($css);
$rendered_css = (string) $this->renderer->renderInIsolation($css_render_array);
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_js = (string) $this->renderer->renderInIsolation($js_render_array);
$query_string = $this->container->get('asset.query_string')->get();
$this->assertStringContainsString('<link rel="stylesheet" media="all" href="' . str_replace('&', '&amp;', $this->fileUrlGenerator->generateString('core/modules/system/tests/modules/common_test/querystring.css?arg1=value1&arg2=value2')) . '&amp;' . $query_string . '" />', $rendered_css, 'CSS file with query string gets version query string correctly appended..');
$this->assertStringContainsString('<script src="' . str_replace('&', '&amp;', $this->fileUrlGenerator->generateString('core/modules/system/tests/modules/common_test/querystring.js?arg1=value1&arg2=value2')) . '&amp;' . $query_string . '"></script>', $rendered_js, 'JavaScript file with query string gets version query string correctly appended.');
}
/**
* Test settings can be loaded even when libraries are not.
*/
public function testAttachedSettingsWithoutLibraries(): void {
$assets = new AttachedAssets();
// First test with no libraries will return no settings.
$assets->setSettings(['test' => 'foo']);
$js = $this->assetResolver->getJsAssets($assets, TRUE, \Drupal::languageManager()->getCurrentLanguage())[1];
$this->assertArrayNotHasKey('drupalSettings', $js);
// Second test with a warm cache.
$js = $this->assetResolver->getJsAssets($assets, TRUE, \Drupal::languageManager()->getCurrentLanguage())[1];
$this->assertArrayNotHasKey('drupalSettings', $js);
// Now test with different settings when drupalSettings is already loaded.
$assets->setSettings(['test' => 'bar']);
$assets->setAlreadyLoadedLibraries(['core/drupalSettings']);
$js = $this->assetResolver->getJsAssets($assets, TRUE, \Drupal::languageManager()->getCurrentLanguage())[1];
$this->assertSame('bar', $js['drupalSettings']['data']['test']);
}
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Asset;
use Drupal\KernelTests\KernelTestBase;
/**
* Checks the status and definition contents of deprecated libraries.
*
* @group Asset
* @group legacy
*/
class DeprecatedAssetsTest extends KernelTestBase {
/**
* Confirms the status and definition contents of deprecated libraries.
*
* @param string $extension
* The name of the extension that registered a library.
* @param string $name
* The name of a registered library to retrieve.
* @param string $deprecation_suffix
* The part of the deprecation message after the extension/name.
* @param string $expected_hashed_library_definition
* The expected MD5 hash of the library.
*
* @dataProvider deprecatedLibrariesProvider
*/
public function testDeprecatedLibraries(string $extension, string $name, string $deprecation_suffix, string $expected_hashed_library_definition): void {
/** @var \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery */
$library_discovery = $this->container->get('library.discovery');
// DrupalCI uses a precision of 100 in certain environments which breaks
// this test.
ini_set('serialize_precision', -1);
$this->expectDeprecation("The $extension/$name " . $deprecation_suffix);
$library_definition = $library_discovery->getLibraryByName($extension, $name);
$this->assertEquals($expected_hashed_library_definition, md5(serialize($library_definition)));
}
/**
* The data provider for testDeprecatedLibraries.
*
* Returns an array in the form of
* @code
* [
* (string) description => [
* (string) extension - The name of the extension that registered a library, usually 'core'
* (string) name - The name of a registered library
* (string) deprecation_suffix - The part of the deprecation message after the extension/name
* (string) expected_hashed_library_definition - The expected MD5 hash of the library
* ]
* ]
* @endcode
*
* @return array
* See description above.
*/
public static function deprecatedLibrariesProvider(): array {
return [
'Tests deprecation of library core/js-cookie' => [
'core',
'js-cookie',
'asset library is deprecated in Drupal 10.1.0 and will be removed in Drupal 11.0.0. There is no replacement. See https://www.drupal.org/node/3322720',
'5d6a84c6143d0fa766cabdb1ff0a270d',
],
];
}
}

View File

@ -0,0 +1,350 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Asset;
use Drupal\Core\Asset\Exception\InvalidLibrariesExtendSpecificationException;
use Drupal\Core\Asset\Exception\InvalidLibrariesOverrideSpecificationException;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the library discovery and library discovery parser.
*
* @group Render
*/
class LibraryDiscoveryIntegrationTest extends KernelTestBase {
/**
* The library discovery service.
*
* @var \Drupal\Core\Asset\LibraryDiscoveryInterface
*/
protected $libraryDiscovery;
/**
* {@inheritdoc}
*/
protected static $modules = ['theme_test'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->container->get('theme_installer')->install(['test_theme', 'starterkit_theme']);
$this->libraryDiscovery = $this->container->get('library.discovery');
}
/**
* Tests that hook_library_info is invoked and the cache is cleared.
*/
public function testHookLibraryInfoByTheme(): void {
// Activate test_theme and verify that the library 'kitten' is added using
// hook_library_info_alter().
$this->activateTheme('test_theme');
$this->assertNotEmpty($this->libraryDiscovery->getLibraryByName('test_theme', 'kitten'));
// Now make starterkit_theme the active theme and assert that library is not
// added.
$this->activateTheme('starterkit_theme');
$this->assertFalse($this->libraryDiscovery->getLibraryByName('test_theme', 'kitten'));
}
/**
* Tests that libraries-override are applied to library definitions.
*/
public function testLibrariesOverride(): void {
// Assert some starterkit_theme libraries that will be overridden or
// removed.
$this->activateTheme('starterkit_theme');
$this->assertAssetInLibrary('core/themes/starterkit_theme/css/components/button.css', 'starterkit_theme', 'base', 'css');
$this->assertAssetInLibrary('core/themes/starterkit_theme/css/components/container-inline.css', 'starterkit_theme', 'base', 'css');
$this->assertAssetInLibrary('core/themes/starterkit_theme/css/components/details.css', 'starterkit_theme', 'base', 'css');
$this->assertAssetInLibrary('core/themes/starterkit_theme/css/components/dialog.css', 'starterkit_theme', 'dialog', 'css');
// Confirmatory assert on core library to be removed.
$this->assertNotEmpty($this->libraryDiscovery->getLibraryByName('core', 'drupal.progress'), 'Confirmatory test on "core/drupal.progress"');
// Activate test theme that defines libraries overrides.
$this->activateTheme('test_theme');
// Assert that entire library was correctly overridden.
$this->assertEquals($this->libraryDiscovery->getLibraryByName('core', 'drupal.collapse'), $this->libraryDiscovery->getLibraryByName('test_theme', 'collapse'), 'Entire library correctly overridden.');
// Assert that starterkit_theme library assets were correctly overridden or
// removed.
$this->assertNoAssetInLibrary('core/themes/starterkit_theme/css/components/button.css', 'starterkit_theme', 'base', 'css');
$this->assertNoAssetInLibrary('core/themes/starterkit_theme/css/components/container-inline.css', 'starterkit_theme', 'base', 'css');
$this->assertNoAssetInLibrary('core/themes/starterkit_theme/css/components/details.css', 'starterkit_theme', 'base', 'css');
$this->assertNoAssetInLibrary('core/themes/starterkit_theme/css/components/dialog.css', 'starterkit_theme', 'dialog', 'css');
$this->assertAssetInLibrary('core/modules/system/tests/themes/test_theme/css/my-button.css', 'starterkit_theme', 'base', 'css');
$this->assertAssetInLibrary('themes/my_theme/css/my-container-inline.css', 'starterkit_theme', 'base', 'css');
$this->assertAssetInLibrary('themes/my_theme/css/my-details.css', 'starterkit_theme', 'base', 'css');
// Assert that entire library was correctly removed.
$this->assertFalse($this->libraryDiscovery->getLibraryByName('core', 'drupal.progress'), 'Entire library correctly removed.');
// Assert that overridden library asset still retains attributes.
$library = $this->libraryDiscovery->getLibraryByName('core', 'drupal.batch');
$this->assertSame('core/modules/system/tests/themes/test_theme/js/collapse.js', $library['js'][0]['data']);
$this->assertFalse($library['js'][0]['cache']);
}
/**
* Tests libraries-override on drupalSettings.
*/
public function testLibrariesOverrideDrupalSettings(): void {
// Activate test theme that attempts to override drupalSettings.
$this->activateTheme('test_theme_libraries_override_with_drupal_settings');
// Assert that drupalSettings cannot be overridden and throws an exception.
try {
$this->libraryDiscovery->getLibraryByName('core', 'drupal.ajax');
$this->fail('Throw Exception when trying to override drupalSettings');
}
catch (InvalidLibrariesOverrideSpecificationException $e) {
$expected_message = 'drupalSettings may not be overridden in libraries-override. Trying to override core/drupal.ajax/drupalSettings. Use hook_library_info_alter() instead.';
$this->assertEquals($expected_message, $e->getMessage(), 'Throw Exception when trying to override drupalSettings');
}
}
/**
* Tests libraries-override on malformed assets.
*/
public function testLibrariesOverrideMalformedAsset(): void {
// Activate test theme that overrides with a malformed asset.
$this->activateTheme('test_theme_libraries_override_with_invalid_asset');
// Assert that improperly formed asset "specs" throw an exception.
try {
$this->libraryDiscovery->getLibraryByName('core', 'drupal.dialog');
$this->fail('Throw Exception when specifying invalid override');
}
catch (InvalidLibrariesOverrideSpecificationException $e) {
$expected_message = 'Library asset core/drupal.dialog/css is not correctly specified. It should be in the form "extension/library_name/sub_key/path/to/asset.js".';
$this->assertEquals($expected_message, $e->getMessage(), 'Throw Exception when specifying invalid override');
}
}
/**
* Tests libraries overrides with multiple parent themes.
*/
public function testLibrariesOverridesMultiple(): void {
/** @var \Drupal\Core\Extension\ThemeInstallerInterface $theme_installer */
$theme_installer = $this->container->get('theme_installer');
$theme_installer->install(['test_base_theme']);
$theme_installer->install(['test_subtheme']);
$theme_installer->install(['test_subsubtheme']);
/** @var \Drupal\Core\Theme\ThemeInitializationInterface $theme_initializer */
$theme_initializer = $this->container->get('theme.initialization');
$active_theme = $theme_initializer->initTheme('test_subsubtheme');
$libraries_override = $active_theme->getLibrariesOverride();
$expected_order = [
'core/modules/system/tests/themes/test_base_theme',
'core/modules/system/tests/themes/test_subtheme',
'core/modules/system/tests/themes/test_subsubtheme',
];
$this->assertEquals($expected_order, array_keys($libraries_override));
}
/**
* Tests library assets with other ways for specifying paths.
*/
public function testLibrariesOverrideOtherAssetLibraryNames(): void {
// Activate a test theme that defines libraries overrides on other types of
// assets.
$this->activateTheme('test_theme');
// Assert Drupal-relative paths.
$this->assertAssetInLibrary('themes/my_theme/css/dropbutton.css', 'core', 'drupal.dropbutton', 'css');
// Assert stream wrapper paths.
$this->assertAssetInLibrary('public://my_css/vertical-tabs.css', 'core', 'drupal.vertical-tabs', 'css');
// Assert a protocol-relative URI.
$this->assertAssetInLibrary('//my-server/my_theme/js/overridden.js', 'core', 'drupal.displace', 'js');
// Assert an absolute URI.
$this->assertAssetInLibrary('http://example.com/my_theme/js/announce.js', 'core', 'drupal.announce', 'js');
}
/**
* Tests that base theme libraries-override still apply in sub themes.
*/
public function testBaseThemeLibrariesOverrideInSubTheme(): void {
// Activate a test theme that has subthemes.
$this->activateTheme('test_subtheme');
// Assert that libraries-override specified in the base theme still applies
// in the sub theme.
$this->assertNoAssetInLibrary('core/misc/dialog/dialog.js', 'core', 'drupal.dialog', 'js');
$this->assertAssetInLibrary('core/modules/system/tests/themes/test_base_theme/js/loadjs.min.js', 'core', 'loadjs', 'js');
}
/**
* Tests libraries-extend.
*/
public function testLibrariesExtend(): void {
// Simulate starterkit_theme defining the test-navigation library.
// @see theme_test_library_info_alter()
$this->container->get('state')
->set('theme_test_library_info_alter starterkit_theme', [
'test-navigation' => [
'css' => [
'component' => [
'css/components/test-navigation.css' => [],
],
],
],
]);
// Activate starterkit_theme and verify the libraries are not extended.
$this->activateTheme('starterkit_theme');
$this->assertNoAssetInLibrary('core/modules/system/tests/themes/test_theme_libraries_extend/css/extend_1.css', 'starterkit_theme', 'test-navigation', 'css');
$this->assertNoAssetInLibrary('core/modules/system/tests/themes/test_theme_libraries_extend/js/extend_1.js', 'starterkit_theme', 'test-navigation', 'js');
$this->assertNoAssetInLibrary('core/modules/system/tests/themes/test_theme_libraries_extend/css/extend_2.css', 'starterkit_theme', 'test-navigation', 'css');
// Activate the theme that extends the test-navigation library in
// starterkit_theme.
$this->activateTheme('test_theme_libraries_extend');
$this->assertAssetInLibrary('core/modules/system/tests/themes/test_theme_libraries_extend/css/extend_1.css', 'starterkit_theme', 'test-navigation', 'css');
$this->assertAssetInLibrary('core/modules/system/tests/themes/test_theme_libraries_extend/js/extend_1.js', 'starterkit_theme', 'test-navigation', 'js');
$this->assertAssetInLibrary('core/modules/system/tests/themes/test_theme_libraries_extend/css/extend_2.css', 'starterkit_theme', 'test-navigation', 'css');
// Activate a sub theme and confirm that it inherits the library assets
// extended in the base theme as well as its own.
$this->assertNoAssetInLibrary('core/modules/system/tests/themes/test_base_theme/css/base-libraries-extend.css', 'starterkit_theme', 'base', 'css');
$this->assertNoAssetInLibrary('core/modules/system/tests/themes/test_subtheme/css/sub-libraries-extend.css', 'starterkit_theme', 'base', 'css');
$this->activateTheme('test_subtheme');
$this->assertAssetInLibrary('core/modules/system/tests/themes/test_base_theme/css/base-libraries-extend.css', 'starterkit_theme', 'base', 'css');
$this->assertAssetInLibrary('core/modules/system/tests/themes/test_subtheme/css/sub-libraries-extend.css', 'starterkit_theme', 'base', 'css');
// Activate test theme that extends with a non-existent library. An
// exception should be thrown.
$this->activateTheme('test_theme_libraries_extend');
try {
$this->libraryDiscovery->getLibraryByName('core', 'drupal.dialog');
$this->fail('Throw Exception when specifying non-existent libraries-extend.');
}
catch (InvalidLibrariesExtendSpecificationException $e) {
$expected_message = 'The specified library "test_theme_libraries_extend/non_existent_library" does not exist.';
$this->assertEquals($expected_message, $e->getMessage(), 'Throw Exception when specifying non-existent libraries-extend.');
}
// Also, test non-string libraries-extend. An exception should be thrown.
$this->container->get('theme_installer')->install(['test_theme']);
try {
$this->libraryDiscovery->getLibraryByName('test_theme', 'collapse');
$this->fail('Throw Exception when specifying non-string libraries-extend.');
}
catch (InvalidLibrariesExtendSpecificationException $e) {
$expected_message = 'The libraries-extend specification for each library must be a list of strings.';
$this->assertEquals($expected_message, $e->getMessage(), 'Throw Exception when specifying non-string libraries-extend.');
}
}
/**
* Test library deprecation support.
*
* @group legacy
*/
public function testDeprecatedLibrary(): void {
$this->expectDeprecation('Targeting theme_test/moved_from css/foo.css from test_theme_with_deprecated_libraries library_overrides is deprecated in drupal:X.0.0 and will be removed in drupal:Y.0.0. Target theme_test/moved_to css/base-remove.css instead. See https://example.com');
$this->expectDeprecation('Targeting theme_test/moved_from js/bar.js from test_theme_with_deprecated_libraries library_overrides is deprecated in drupal:X.0.0 and will be removed in drupal:Y.0.0. Target theme_test/moved_to js/foo.js instead. See https://example.com');
$this->expectDeprecation('Theme "theme_test" is overriding a deprecated library. The "theme_test/deprecated_library" asset library is deprecated in drupal:X.0.0 and is removed from drupal:Y.0.0. Use another library instead. See https://www.example.com');
$this->expectDeprecation('Theme "theme_test" is extending a deprecated library. The "theme_test/another_deprecated_library" asset library is deprecated in drupal:X.0.0 and is removed from drupal:Y.0.0. Use another library instead. See https://www.example.com');
$this->expectDeprecation('The "theme_test/deprecated_library" asset library is deprecated in drupal:X.0.0 and is removed from drupal:Y.0.0. Use another library instead. See https://www.example.com');
$this->expectDeprecation('The "theme_test/another_deprecated_library" asset library is deprecated in drupal:X.0.0 and is removed from drupal:Y.0.0. Use another library instead. See https://www.example.com');
$this->activateTheme('test_theme_with_deprecated_libraries');
$this->libraryDiscovery->getLibraryByName('theme_test', 'moved_to');
$this->libraryDiscovery->getLibraryByName('theme_test', 'deprecated_library');
$this->libraryDiscovery->getLibraryByName('theme_test', 'another_deprecated_library');
}
/**
* Activates a specified theme.
*
* Installs the theme if not already installed and makes it the active theme.
*
* @param string $theme_name
* The name of the theme to be activated.
*/
protected function activateTheme($theme_name): void {
$this->container->get('theme_installer')->install([$theme_name]);
/** @var \Drupal\Core\Theme\ThemeInitializationInterface $theme_initializer */
$theme_initializer = $this->container->get('theme.initialization');
/** @var \Drupal\Core\Theme\ThemeManagerInterface $theme_manager */
$theme_manager = $this->container->get('theme.manager');
$theme_manager->setActiveTheme($theme_initializer->getActiveThemeByName($theme_name));
$this->libraryDiscovery->clear();
$this->assertSame($theme_name, $theme_manager->getActiveTheme()->getName());
}
/**
* Asserts that the specified asset is in the given library.
*
* @param string $asset
* The asset file with the path for the file.
* @param string $extension
* The extension in which the $library is defined.
* @param string $library_name
* Name of the library.
* @param string $sub_key
* The library sub key where the given asset is defined.
* @param string $message
* (optional) A message to display with the assertion.
*
* @internal
*/
protected function assertAssetInLibrary(string $asset, string $extension, string $library_name, string $sub_key, ?string $message = NULL): void {
if (!isset($message)) {
$message = sprintf('Asset %s found in library "%s/%s"', $asset, $extension, $library_name);
}
$library = $this->libraryDiscovery->getLibraryByName($extension, $library_name);
foreach ($library[$sub_key] as $definition) {
if ($asset == $definition['data']) {
return;
}
}
$this->fail($message);
}
/**
* Asserts that the specified asset is not in the given library.
*
* @param string $asset
* The asset file with the path for the file.
* @param string $extension
* The extension in which the $library_name is defined.
* @param string $library_name
* Name of the library.
* @param string $sub_key
* The library sub key where the given asset is defined.
* @param string $message
* (optional) A message to display with the assertion.
*
* @internal
*/
protected function assertNoAssetInLibrary(string $asset, string $extension, string $library_name, string $sub_key, ?string $message = NULL): void {
if (!isset($message)) {
$message = sprintf('Asset %s not found in library "%s/%s"', $asset, $extension, $library_name);
}
$library = $this->libraryDiscovery->getLibraryByName($extension, $library_name);
foreach ($library[$sub_key] as $definition) {
if ($asset == $definition['data']) {
$this->fail($message);
}
}
}
}

View File

@ -0,0 +1,263 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Asset;
use Drupal\Core\Extension\ExtensionLifecycle;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests that the asset files for all core libraries exist.
*
* This test also changes the active theme to each core theme to verify
* the libraries after theme-level libraries-override and libraries-extend are
* applied.
*
* @group Asset
* @group #slow
*/
class ResolvedLibraryDefinitionsFilesMatchTest extends KernelTestBase {
/**
* The theme handler.
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface
*/
protected $themeHandler;
/**
* The theme initialization.
*
* @var \Drupal\Core\Theme\ThemeInitializationInterface
*/
protected $themeInitialization;
/**
* The theme manager.
*
* @var \Drupal\Core\Theme\ThemeManagerInterface
*/
protected $themeManager;
/**
* The library discovery service.
*
* @var \Drupal\Core\Asset\LibraryDiscoveryInterface
*/
protected $libraryDiscovery;
/**
* A list of all core modules.
*
* @var string[]
*/
protected $allModules;
/**
* A list of all core themes.
*
* We hardcode this because test themes don't use a 'package' or 'hidden' key
* so we don't have a good way of filtering to only get "real" themes.
*
* @var string[]
*/
protected $allThemes = [
'claro',
'olivero',
'stable9',
'stark',
];
/**
* A list of libraries to skip checking, in the format extension/library_name.
*
* @var string[]
*/
protected $librariesToSkip = [
// Locale has a "dummy" library that does not actually exist.
'locale/translations',
// Core has a "dummy" library that does not actually exist.
'core/ckeditor5.translations',
];
/**
* A list of all paths that have been checked.
*
* @var array[]
*/
protected $pathsChecked;
/**
* {@inheritdoc}
*/
protected static $modules = ['system', 'user', 'path_alias'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Install all core themes.
sort($this->allThemes);
$this->container->get('theme_installer')->install($this->allThemes);
$this->themeHandler = $this->container->get('theme_handler');
$this->themeInitialization = $this->container->get('theme.initialization');
$this->themeManager = $this->container->get('theme.manager');
$this->libraryDiscovery = $this->container->get('library.discovery');
}
/**
* Ensures that all core module and theme library files exist.
*/
public function testCoreLibraryCompleteness(): void {
// Enable all core modules.
$all_modules = $this->container->get('extension.list.module')->getList();
$all_modules = array_filter($all_modules, function ($module) {
// Filter contrib, hidden, already enabled modules and modules in the
// Testing package.
if ($module->origin !== 'core'
|| !empty($module->info['hidden'])
|| $module->status == TRUE
|| $module->info['package'] == 'Testing'
|| $module->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::EXPERIMENTAL
|| $module->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::DEPRECATED) {
return FALSE;
}
return TRUE;
});
// Install the 'user' entity schema because the workspaces module's install
// hook creates a workspace with default uid of 1. Then the layout_builder
// module's implementation of hook_entity_presave will cause
// \Drupal\Core\TypedData\Validation\RecursiveValidator::validate() to run
// on the workspace which will fail because the user table is not present.
// @todo Remove this in https://www.drupal.org/node/3039217.
$this->installEntitySchema('user');
// Install the 'path_alias' entity schema because the path alias path
// processor requires it.
$this->installEntitySchema('path_alias');
// Remove demo_umami_content module as its install hook creates content
// that relies on the presence of entity tables and various other elements
// not present in a kernel test.
unset($all_modules['demo_umami_content']);
$this->allModules = array_keys($all_modules);
$this->allModules[] = 'system';
$this->allModules[] = 'user';
$this->allModules[] = 'path_alias';
$database_module = \Drupal::database()->getProvider();
if ($database_module !== 'core') {
$this->allModules[] = $database_module;
}
sort($this->allModules);
$this->container->get('module_installer')->install($this->allModules);
// Get a library discovery from the new container.
$this->libraryDiscovery = $this->container->get('library.discovery');
$this->assertLibraries();
}
/**
* Ensures that module and theme library files exist for a deprecated modules.
*
* @group legacy
*/
public function testCoreLibraryCompletenessDeprecated(): void {
// Find and install deprecated modules to test.
$all_modules = $this->container->get('extension.list.module')->getList();
$deprecated_modules_to_test = array_filter($all_modules, function ($module) {
if ($module->origin == 'core'
&& $module->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::DEPRECATED) {
return TRUE;
}
});
$this->container->get('module_installer')->install(array_keys($deprecated_modules_to_test));
$this->libraryDiscovery = $this->container->get('library.discovery');
$this->allModules = array_keys(\Drupal::moduleHandler()->getModuleList());
$this->assertLibraries();
}
/**
* Asserts the libraries for modules and themes exist.
*/
public function assertLibraries(): void {
// First verify all libraries with no active theme.
$this->verifyLibraryFilesExist($this->getAllLibraries());
// Then verify all libraries for each core theme. This may seem like
// overkill but themes can override and extend other extensions' libraries
// and these changes are only applied for the active theme.
foreach ($this->allThemes as $theme) {
$this->themeManager->setActiveTheme($this->themeInitialization->getActiveThemeByName($theme));
$this->libraryDiscovery->clear();
$this->verifyLibraryFilesExist($this->getAllLibraries());
}
}
/**
* Checks that all the library files exist.
*
* @param array[] $library_definitions
* An array of library definitions, keyed by extension, then by library, and
* so on.
*/
protected function verifyLibraryFilesExist($library_definitions): void {
foreach ($library_definitions as $extension => $libraries) {
foreach ($libraries as $library_name => $library) {
if (in_array("$extension/$library_name", $this->librariesToSkip)) {
continue;
}
// Check that all the assets exist.
foreach (['css', 'js'] as $asset_type) {
foreach ($library[$asset_type] as $asset) {
$file = $asset['data'];
$path = $this->root . '/' . $file;
// Only check and assert each file path once.
if (!isset($this->pathsChecked[$path])) {
$this->assertFileExists($path, "$file file referenced from the $extension/$library_name library does not exist.");
$this->pathsChecked[$path] = TRUE;
}
}
}
}
}
}
/**
* Gets all libraries for core and all installed modules.
*
* @return \Drupal\Core\Extension\Extension[]
* An array of discovered libraries keyed by extension name.
*/
protected function getAllLibraries() {
$modules = \Drupal::moduleHandler()->getModuleList();
$extensions = $modules;
$module_list = array_keys($modules);
sort($module_list);
$this->assertEquals($this->allModules, $module_list, 'All core modules are installed.');
$themes = $this->themeHandler->listInfo();
$extensions += $themes;
$theme_list = array_keys($themes);
sort($theme_list);
$this->assertEquals($this->allThemes, $theme_list, 'All core themes are installed.');
$libraries['core'] = $this->libraryDiscovery->getLibrariesByExtension('core');
foreach ($extensions as $extension_name => $extension) {
$library_file = $extension->getPath() . '/' . $extension_name . '.libraries.yml';
if (is_file($this->root . '/' . $library_file)) {
$libraries[$extension_name] = $this->libraryDiscovery->getLibrariesByExtension($extension_name);
}
}
return $libraries;
}
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Batch;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests batch functionality.
*
* @group Batch
*/
class BatchKernelTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
require_once $this->root . '/core/includes/batch.inc';
}
/**
* Tests _batch_needs_update().
*/
public function testNeedsUpdate(): void {
// Before ever being called, the return value should be FALSE.
$this->assertEquals(FALSE, _batch_needs_update());
// Set the value to TRUE.
$this->assertEquals(TRUE, _batch_needs_update(TRUE));
// Check that without a parameter TRUE is returned.
$this->assertEquals(TRUE, _batch_needs_update());
// Set the value to FALSE.
$this->assertEquals(FALSE, _batch_needs_update(FALSE));
$this->assertEquals(FALSE, _batch_needs_update());
}
}

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Batch;
use Drupal\Core\Routing\RouteMatch;
use Drupal\KernelTests\KernelTestBase;
use Symfony\Component\HttpFoundation\Request;
/**
* Tests the BatchNegotiator.
*
* @group Batch
*/
class BatchNegotiatorTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
];
/**
* Test that the negotiator applies to the batch route.
*/
public function testApplies(): void {
$request = Request::create('/batch');
// Use the router to enhance the object so that a RouteMatch can be created.
$this->container->get('router')->matchRequest($request);
$routeMatch = RouteMatch::createFromRequest($request);
// The negotiator under test.
$negotiator = $this->container->get('theme.negotiator.system.batch');
$this->assertTrue($negotiator->applies($routeMatch));
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Block;
use Drupal\Core\Form\FormState;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the BlockBase class, base for all block plugins.
*
* @group block
*/
class BlockBaseTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system', 'block', 'block_test'];
/**
* Tests that blocks config form have context mapping, and it is stored in configuration.
*/
public function testContextMapping(): void {
$configuration = ['label' => 'A very cool block'];
/** @var \Drupal\Core\Block\BlockManagerInterface $blockManager */
$blockManager = \Drupal::service('plugin.manager.block');
/** @var \Drupal\Core\Block\BlockBase $block */
$block = $blockManager->createInstance('test_block_instantiation', $configuration);
// Check that context mapping is present in the block config form.
$form = [];
$form_state = new FormState();
$form = $block->buildConfigurationForm($form, $form_state);
$this->assertArrayHasKey('context_mapping', $form);
// Check that context mapping is stored in block's configuration.
$context_mapping = [
'user' => 'current_user',
];
$form_state->setValue('context_mapping', $context_mapping);
$block->submitConfigurationForm($form, $form_state);
$this->assertEquals($context_mapping, $block->getConfiguration()['context_mapping'] ?? NULL);
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Block;
use Drupal\block_test\PluginForm\EmptyBlockForm;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests that blocks can have multiple forms.
*
* @group block
*/
class MultipleBlockFormTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system', 'block', 'block_test'];
/**
* Tests that blocks can have multiple forms.
*/
public function testMultipleForms(): void {
$configuration = ['label' => 'A very cool block'];
$block = \Drupal::service('plugin.manager.block')->createInstance('test_multiple_forms_block', $configuration);
$form_object1 = \Drupal::service('plugin_form.factory')->createInstance($block, 'configure');
$form_object2 = \Drupal::service('plugin_form.factory')->createInstance($block, 'secondary');
// Assert that the block itself is used for the default form.
$this->assertSame($block, $form_object1);
// Ensure that EmptyBlockForm is used and the plugin is set.
$this->assertInstanceOf(EmptyBlockForm::class, $form_object2);
$this->assertEquals($block, $form_object2->plugin);
}
}

View File

@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Bootstrap;
use Drupal\Core\Extension\Exception\UnknownExtensionException;
use Drupal\Core\Extension\Exception\UnknownExtensionTypeException;
use Drupal\Core\Extension\ExtensionPathResolver;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ProfileExtensionList;
use Drupal\Core\Extension\ThemeEngineExtensionList;
use Drupal\Core\Extension\ThemeExtensionList;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests that extension path resolver works correctly.
*
* @coversDefaultClass \Drupal\Core\Extension\ExtensionPathResolver
*
* @group Bootstrap
*/
class ExtensionPathResolverTest extends KernelTestBase {
/**
* @covers ::getPathname
*/
public function testExtensionPathResolving(): void {
// Retrieving the location of a module.
$this->assertSame('core/modules/system/system.info.yml', \Drupal::service('extension.list.module')
->getPathname('system'));
// Retrieving the location of a theme.
\Drupal::service('theme_installer')->install(['stark']);
$this->assertSame('core/themes/stark/stark.info.yml', \Drupal::service('extension.list.theme')
->getPathname('stark'));
// Retrieving the location of a theme engine.
$this->assertSame('core/themes/engines/twig/twig.info.yml', \Drupal::service('extension.list.theme_engine')
->getPathname('twig'));
// Retrieving the location of a profile. Profiles are a special case with
// a fixed location and naming.
$this->assertSame('core/profiles/tests/testing/testing.info.yml', \Drupal::service('extension.list.profile')
->getPathname('testing'));
}
/**
* @covers ::getPath
*/
public function testExtensionPathResolvingPath(): void {
$this->assertSame('core/modules/system/tests/modules/driver_test', \Drupal::service('extension.list.module')
->getPath('driver_test'));
}
/**
* @covers ::getPathname
*/
public function testExtensionPathResolvingWithNonExistingModule(): void {
$this->expectException(UnknownExtensionException::class);
$this->expectExceptionMessage('The module there_is_a_module_for_that does not exist.');
$this->assertNull(\Drupal::service('extension.list.module')
->getPathname('there_is_a_module_for_that'), 'Searching for an item that does not exist returns NULL.');
}
/**
* @covers ::getPathname
*/
public function testExtensionPathResolvingWithNonExistingTheme(): void {
$this->expectException(UnknownExtensionException::class);
$this->expectExceptionMessage('The theme there_is_a_theme_for_you does not exist.');
$this->assertNull(\Drupal::service('extension.list.theme')
->getPathname('there_is_a_theme_for_you'), 'Searching for an item that does not exist returns NULL.');
}
/**
* @covers ::getPathname
*/
public function testExtensionPathResolvingWithNonExistingProfile(): void {
$this->expectException(UnknownExtensionException::class);
$this->expectExceptionMessage('The profile there_is_an_install_profile_for_you does not exist.');
$this->assertNull(\Drupal::service('extension.list.profile')
->getPathname('there_is_an_install_profile_for_you'), 'Searching for an item that does not exist returns NULL.');
}
/**
* @covers ::getPathname
*/
public function testExtensionPathResolvingWithNonExistingThemeEngine(): void {
$this->expectException(UnknownExtensionException::class);
$this->expectExceptionMessage('The theme_engine there_is_an_theme_engine_for_you does not exist');
$this->assertNull(\Drupal::service('extension.list.theme_engine')
->getPathname('there_is_an_theme_engine_for_you'), 'Searching for an item that does not exist returns NULL.');
}
/**
* Tests the getPath() method with an unknown extension.
*/
public function testUnknownExtension(): void {
$module_extension_list = $this->prophesize(ModuleExtensionList::class);
$profile_extension_list = $this->prophesize(ProfileExtensionList::class);
$theme_extension_list = $this->prophesize(ThemeExtensionList::class);
$theme_engine_extension_list = $this->prophesize(ThemeEngineExtensionList::class);
$resolver = new ExtensionPathResolver(
$module_extension_list->reveal(),
$profile_extension_list->reveal(),
$theme_extension_list->reveal(),
$theme_engine_extension_list->reveal()
);
$this->expectException(UnknownExtensionTypeException::class);
$this->expectExceptionMessage('Extension type foo is unknown.');
$resolver->getPath('foo', 'bar');
}
}

View File

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Bootstrap;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests that drupal_static() and drupal_static_reset() work.
*
* @group Bootstrap
*/
class ResettableStaticTest extends KernelTestBase {
/**
* Tests drupal_static() function.
*
* Tests that a variable reference returned by drupal_static() gets reset when
* drupal_static_reset() is called.
*/
public function testDrupalStatic(): void {
$name = __CLASS__ . '_' . __METHOD__;
$var = &drupal_static($name, 'foo');
$this->assertEquals('foo', $var, 'Variable returned by drupal_static() was set to its default.');
// Call the specific reset and the global reset each twice to ensure that
// multiple resets can be issued without odd side effects.
$var = 'bar';
drupal_static_reset($name);
$this->assertEquals('foo', $var, 'Variable was reset after first invocation of name-specific reset.');
$var = 'bar';
drupal_static_reset($name);
$this->assertEquals('foo', $var, 'Variable was reset after second invocation of name-specific reset.');
$var = 'bar';
drupal_static_reset();
$this->assertEquals('foo', $var, 'Variable was reset after first invocation of global reset.');
$var = 'bar';
drupal_static_reset();
$this->assertEquals('foo', $var, 'Variable was reset after second invocation of global reset.');
// Test calling drupal_static() with no arguments (empty string).
$name1 = __CLASS__ . '_' . __METHOD__ . '1';
$name2 = '';
$var1 = &drupal_static($name1, 'initial1');
$var2 = &drupal_static($name2, 'initial2');
$this->assertEquals('initial1', $var1, 'Variable 1 returned by drupal_static() was set to its default.');
$this->assertEquals('initial2', $var2, 'Variable 2 returned by drupal_static() was set to its default.');
$var1 = 'modified1';
$var2 = 'modified2';
drupal_static_reset($name1);
drupal_static_reset($name2);
$this->assertEquals('initial1', $var1, 'Variable 1 was reset after invocation of name-specific reset.');
$this->assertEquals('initial2', $var2, 'Variable 2 was reset after invocation of name-specific reset.');
$var1 = 'modified1';
$var2 = 'modified2';
drupal_static_reset();
$this->assertEquals('initial1', $var1, 'Variable 1 was reset after invocation of global reset.');
$this->assertEquals('initial2', $var2, 'Variable 2 was reset after invocation of global reset.');
}
}

View File

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Bootstrap;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests.
*
* @group Bootstrap
*/
class ShutdownFunctionTest extends KernelTestBase {
/**
* Flag to indicate if ::shutdownOne() called.
*
* @var bool
*/
protected $shutDownOneCalled = FALSE;
/**
* Flag to indicate if ::shutdownTwo() called.
*
* @var bool
*/
protected $shutDownTwoCalled = FALSE;
/**
* Tests that shutdown functions can be added by other shutdown functions.
*/
public function testShutdownFunctionInShutdownFunction(): void {
// Ensure there are no shutdown functions registered before starting the
// test.
$this->assertEmpty(drupal_register_shutdown_function());
// Register a shutdown function that, when called, will register another
// shutdown function.
drupal_register_shutdown_function([$this, 'shutdownOne']);
$this->assertCount(1, drupal_register_shutdown_function());
// Simulate the Drupal shutdown.
_drupal_shutdown_function();
// Test that the expected functions are called.
$this->assertTrue($this->shutDownOneCalled);
$this->assertTrue($this->shutDownTwoCalled);
$this->assertCount(2, drupal_register_shutdown_function());
}
/**
* Tests shutdown functions by registering another shutdown function.
*/
public function shutdownOne(): void {
drupal_register_shutdown_function([$this, 'shutdownTwo']);
$this->shutDownOneCalled = TRUE;
}
/**
* Tests shutdown functions by being registered during shutdown.
*/
public function shutdownTwo(): void {
$this->shutDownTwoCalled = TRUE;
}
}

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Cache;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Cache\ApcuBackend;
/**
* Tests the APCu cache backend.
*
* @group Cache
* @requires extension apcu
*/
class ApcuBackendTest extends GenericCacheBackendUnitTestBase {
/**
* {@inheritdoc}
*/
protected function createCacheBackend($bin) {
return new ApcuBackend($bin, $this->databasePrefix, \Drupal::service('cache_tags.invalidator.checksum'), \Drupal::service(TimeInterface::class));
}
/**
* {@inheritdoc}
*/
protected function tearDown(): void {
foreach ($this->cacheBackends as $bin => $cache_backend) {
$this->cacheBackends[$bin]->removeBin();
}
parent::tearDown();
}
/**
* {@inheritdoc}
*/
public function testSetGet(): void {
parent::testSetGet();
// Make sure entries are permanent (i.e. no TTL).
$backend = $this->getCacheBackend($this->getTestBin());
$key = $backend->getApcuKey('TEST8');
$iterator = new \APCUIterator('/^' . $key . '/');
foreach ($iterator as $item) {
$this->assertEquals(0, $item['ttl']);
$found = TRUE;
}
$this->assertTrue($found);
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Cache;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Cache\BackendChain;
use Drupal\Core\Cache\MemoryBackend;
/**
* Unit test of the backend chain using the generic cache unit test base.
*
* @group Cache
*/
class BackendChainTest extends GenericCacheBackendUnitTestBase {
/**
* {@inheritdoc}
*/
protected function createCacheBackend($bin) {
$chain = new BackendChain();
// We need to create some various backends in the chain.
$time = \Drupal::service(TimeInterface::class);
$chain
->appendBackend(new MemoryBackend($time))
->prependBackend(new MemoryBackend($time))
->appendBackend(new MemoryBackend($time));
\Drupal::service('cache_tags.invalidator')->addInvalidator($chain);
return $chain;
}
}

View File

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Cache;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\Core\Cache\CacheCollectorHelper;
use Drupal\TestTools\Random;
use Symfony\Component\DependencyInjection\Reference;
/**
* Tests DatabaseBackend cache tag implementation.
*
* @group Cache
*/
class CacheCollectorTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public function register(ContainerBuilder $container): void {
parent::register($container);
// Change container to database cache backends.
$container
->register('cache_factory', 'Drupal\Core\Cache\CacheFactory')
->addArgument(new Reference('settings'))
->addMethodCall('setContainer', [new Reference('service_container')]);
// Change container to use database lock backends.
$container
->register('lock', 'Drupal\Core\Lock\DatabaseLockBackend')
->addArgument(new Reference('database'));
}
/**
* Tests setting and invalidating.
*
* @dataProvider providerTestInvalidCharacters
*/
public function testCacheCollector($cid, $key, $value): void {
$collector = new CacheCollectorHelper($cid, $this->container->get('cache.default'), $this->container->get('lock'));
$this->assertNull($collector->get($key));
$collector->set($key, $value);
$this->assertEquals($value, $collector->get($key));
$collector->destruct();
// @todo Shouldn't this be empty after destruction?
$this->assertEquals($value, $collector->get($key));
}
/**
* Data provider for ::testCacheCollector().
*/
public static function providerTestInvalidCharacters() {
return [
// Nothing special.
['foo', 'bar', 'baz'],
// Invalid characters in CID.
// cSpell:disable-next-line
['éøïвβ中國書۞', 'foo', 'bar'],
// Really long CID.
[Random::string(1024), 'foo', 'bar'],
];
}
}

View File

@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Cache;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\user\Entity\Role;
/**
* Tests the cache context optimization.
*
* @group Render
*/
class CacheContextOptimizationTest extends KernelTestBase {
use UserCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['user', 'system'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installConfig(['user']);
}
/**
* Ensures that 'user.permissions' cache context is able to define cache tags.
*/
public function testUserPermissionCacheContextOptimization(): void {
$user1 = $this->createUser();
$this->assertEquals(1, $user1->id());
$authenticated_user = $this->createUser(['administer permissions']);
$role = $authenticated_user->getRoles()[1];
$test_element = [
'#cache' => [
'keys' => ['test'],
'contexts' => ['user', 'user.permissions'],
],
];
\Drupal::service('account_switcher')->switchTo($authenticated_user);
$element = $test_element;
$element['#markup'] = 'content for authenticated users';
$output = \Drupal::service('renderer')->renderRoot($element);
$this->assertEquals('content for authenticated users', $output);
// Verify that the render caching is working so that other tests can be
// trusted.
$element = $test_element;
$element['#markup'] = 'this should not be visible';
$output = \Drupal::service('renderer')->renderRoot($element);
$this->assertEquals('content for authenticated users', $output);
// Even though the cache contexts have been optimized to only include 'user'
// cache context, the element should have been changed because
// 'user.permissions' cache context defined a cache tags for permission
// changes, which should have bubbled up for the element when it was
// optimized away.
Role::load($role)
->revokePermission('administer permissions')
->save();
$element = $test_element;
$element['#markup'] = 'this should be visible';
$output = \Drupal::service('renderer')->renderRoot($element);
$this->assertEquals('this should be visible', $output);
}
/**
* Ensures that 'user.roles' still works when it is optimized away.
*/
public function testUserRolesCacheContextOptimization(): void {
$root_user = $this->createUser();
$this->assertEquals(1, $root_user->id());
$authenticated_user = $this->createUser(['administer permissions']);
$role = $authenticated_user->getRoles()[1];
$test_element = [
'#cache' => [
'keys' => ['test'],
'contexts' => ['user', 'user.roles'],
],
];
\Drupal::service('account_switcher')->switchTo($authenticated_user);
$element = $test_element;
$element['#markup'] = 'content for authenticated users';
$output = \Drupal::service('renderer')->renderRoot($element);
$this->assertEquals('content for authenticated users', $output);
// Verify that the render caching is working so that other tests can be
// trusted.
$element = $test_element;
$element['#markup'] = 'this should not be visible';
$output = \Drupal::service('renderer')->renderRoot($element);
$this->assertEquals('content for authenticated users', $output);
// Even though the cache contexts have been optimized to only include 'user'
// cache context, the element should have been changed because 'user.roles'
// cache context defined a cache tag for user entity changes, which should
// have bubbled up for the element when it was optimized away.
$authenticated_user->removeRole($role)->save();
$element = $test_element;
$element['#markup'] = 'this should be visible';
$output = \Drupal::service('renderer')->renderRoot($element);
$this->assertEquals('this should be visible', $output);
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Cache;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Cache\ChainedFastBackend;
use Drupal\Core\Cache\DatabaseBackend;
use Drupal\Core\Cache\PhpBackend;
/**
* Unit test of the fast chained backend using the generic cache unit test base.
*
* @group Cache
*/
class ChainedFastBackendTest extends GenericCacheBackendUnitTestBase {
/**
* Creates a new instance of ChainedFastBackend.
*
* @return \Drupal\Core\Cache\ChainedFastBackend
* A new ChainedFastBackend object.
*/
protected function createCacheBackend($bin) {
$consistent_backend = new DatabaseBackend(\Drupal::service('database'), \Drupal::service('cache_tags.invalidator.checksum'), $bin, \Drupal::service('serialization.phpserialize'), \Drupal::service(TimeInterface::class), 100);
$fast_backend = new PhpBackend($bin, \Drupal::service('cache_tags.invalidator.checksum'), \Drupal::service(TimeInterface::class));
$backend = new ChainedFastBackend($consistent_backend, $fast_backend, $bin);
// Explicitly register the cache bin as it can not work through the
// cache bin list in the container.
\Drupal::service('cache_tags.invalidator')->addInvalidator($backend);
return $backend;
}
}

View File

@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Cache;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheTagsPurgeInterface;
use Drupal\Core\Database\Database;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\KernelTests\KernelTestBase;
use Symfony\Component\DependencyInjection\Reference;
/**
* Tests DatabaseBackend cache tag implementation.
*
* @group Cache
*/
class DatabaseBackendTagTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system'];
/**
* {@inheritdoc}
*/
public function register(ContainerBuilder $container): void {
parent::register($container);
// Change container to database cache backends.
$container
->register('cache_factory', 'Drupal\Core\Cache\CacheFactory')
->addArgument(new Reference('settings'))
->addMethodCall('setContainer', [new Reference('service_container')]);
}
/**
* Test tag invalidation.
*/
public function testTagInvalidations(): void {
// Create cache entry in multiple bins.
$tags = ['test_tag:1', 'test_tag:2', 'test_tag:3'];
$bins = ['data', 'bootstrap', 'render'];
foreach ($bins as $bin) {
$bin = \Drupal::cache($bin);
$bin->set('test', 'value', Cache::PERMANENT, $tags);
$this->assertNotEmpty($bin->get('test'), 'Cache item was set in bin.');
}
$connection = Database::getConnection();
$invalidations_before = intval($connection->select('cachetags')->fields('cachetags', ['invalidations'])->condition('tag', 'test_tag:2')->execute()->fetchField());
Cache::invalidateTags(['test_tag:2']);
// Test that cache entry has been invalidated in multiple bins.
foreach ($bins as $bin) {
$bin = \Drupal::cache($bin);
$this->assertFalse($bin->get('test'), 'Tag invalidation affected item in bin.');
}
// Test that only one tag invalidation has occurred.
$invalidations_after = intval($connection->select('cachetags')->fields('cachetags', ['invalidations'])->condition('tag', 'test_tag:2')->execute()->fetchField());
$this->assertEquals($invalidations_before + 1, $invalidations_after, 'Only one addition cache tag invalidation has occurred after invalidating a tag used in multiple bins.');
}
/**
* Test cache tag purging.
*/
public function testTagsPurge(): void {
$tags = ['test_tag:1', 'test_tag:2', 'test_tag:3'];
/** @var \Drupal\Core\Cache\CacheTagsChecksumInterface $checksum_invalidator */
$checksum_invalidator = \Drupal::service('cache_tags.invalidator.checksum');
// Assert that initial current tag checksum is 0. This also ensures that the
// 'cachetags' table is created, which at this point does not exist yet.
$this->assertEquals(0, $checksum_invalidator->getCurrentChecksum($tags));
/** @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface $invalidator */
$invalidator = \Drupal::service('cache_tags.invalidator');
$invalidator->invalidateTags($tags);
// Checksum should be incremented by 1 by the invalidation for each tag.
$this->assertEquals(3, $checksum_invalidator->getCurrentChecksum($tags));
// After purging, confirm checksum is 0 and the 'cachetags' table is empty.
$this->assertInstanceOf(CacheTagsPurgeInterface::class, $invalidator);
$invalidator->purge();
$this->assertEquals(0, $checksum_invalidator->getCurrentChecksum($tags));
$rows = Database::getConnection()->select('cachetags')
->fields('cachetags')
->countQuery()
->execute()
->fetchField();
$this->assertEmpty($rows, 'cachetags table is empty.');
}
}

View File

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Cache;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Cache\DatabaseBackend;
/**
* Unit test of the database backend using the generic cache unit test base.
*
* @group Cache
*/
class DatabaseBackendTest extends GenericCacheBackendUnitTestBase {
/**
* The max rows to use for test bins.
*
* @var int
*/
protected static $maxRows = 100;
/**
* {@inheritdoc}
*/
protected static $modules = ['system'];
/**
* Creates a new instance of DatabaseBackend.
*
* @return \Drupal\Core\Cache\DatabaseBackend
* A new DatabaseBackend object.
*/
protected function createCacheBackend($bin) {
return new DatabaseBackend(
$this->container->get('database'),
$this->container->get('cache_tags.invalidator.checksum'),
$bin,
$this->container->get('serialization.phpserialize'),
\Drupal::service(TimeInterface::class),
static::$maxRows,
);
}
/**
* {@inheritdoc}
*/
public function testSetGet(): void {
parent::testSetGet();
$backend = $this->getCacheBackend();
// Set up a cache ID that is not ASCII and longer than 255 characters so we
// can test cache ID normalization.
$cid_long = str_repeat('愛€', 500);
$cached_value_long = $this->randomMachineName();
$backend->set($cid_long, $cached_value_long);
$this->assertSame($cached_value_long, $backend->get($cid_long)->data, "Backend contains the correct value for long, non-ASCII cache id.");
$cid_short = '愛1€';
$cached_value_short = $this->randomMachineName();
$backend->set($cid_short, $cached_value_short);
$this->assertSame($cached_value_short, $backend->get($cid_short)->data, "Backend contains the correct value for short, non-ASCII cache id.");
// Set multiple items to test exceeding the chunk size.
$backend->deleteAll();
$items = [];
for ($i = 0; $i <= DatabaseBackend::MAX_ITEMS_PER_CACHE_SET; $i++) {
$items["test$i"]['data'] = $i;
}
$backend->setMultiple($items);
$this->assertSame(DatabaseBackend::MAX_ITEMS_PER_CACHE_SET + 1, $this->getNumRows());
}
/**
* Tests the row count limiting of cache bin database tables.
*/
public function testGarbageCollection(): void {
$backend = $this->getCacheBackend();
$max_rows = static::$maxRows;
$this->assertSame(0, (int) $this->getNumRows());
// Fill to just the limit.
for ($i = 0; $i < $max_rows; $i++) {
// Ensure that each cache item created happens in a different millisecond,
// by waiting 1 ms (1000 microseconds). The garbage collection might
// otherwise keep less than exactly 100 records (which is acceptable for
// real-world cases, but not for this test).
usleep(1000);
$backend->set("test$i", $i);
}
$this->assertSame($max_rows, $this->getNumRows());
// Garbage collection has no effect.
$backend->garbageCollection();
$this->assertSame($max_rows, $this->getNumRows());
// Go one row beyond the limit.
$backend->set('test' . ($max_rows + 1), $max_rows + 1);
$this->assertSame($max_rows + 1, $this->getNumRows());
// Garbage collection removes one row: the oldest.
$backend->garbageCollection();
$this->assertSame($max_rows, $this->getNumRows());
$this->assertFalse($backend->get('test0'));
}
/**
* Gets the number of rows in the test cache bin database table.
*
* @return int
* The number of rows in the test cache bin database table.
*/
protected function getNumRows(): int {
$table = 'cache_' . $this->testBin;
$connection = $this->container->get('database');
$query = $connection->select($table);
$query->addExpression('COUNT([cid])', 'cid');
return (int) $query->execute()->fetchField();
}
/**
* Tests that "cache_tags.invalidator.checksum" is backend overridable.
*/
public function testCacheTagsInvalidatorChecksumIsBackendOverridable(): void {
$definition = $this->container->getDefinition('cache_tags.invalidator.checksum');
$this->assertTrue($definition->hasTag('backend_overridable'));
}
/**
* Test that the service "cache.backend.database" is backend overridable.
*/
public function testCacheBackendDatabaseIsBackendOverridable(): void {
$definition = $this->container->getDefinition('cache.backend.database');
$this->assertTrue($definition->hasTag('backend_overridable'));
}
}

View File

@ -0,0 +1,195 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Cache;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\DatabaseBackendFactory;
use Drupal\Core\Database\Database;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\KernelTests\KernelTestBase;
use Drupal\user\Entity\User;
use Symfony\Component\DependencyInjection\Reference;
use Drupal\Component\Serialization\PhpSerialize;
/**
* Tests delaying of cache tag invalidation queries to the end of transactions.
*
* @group Cache
*/
class EndOfTransactionQueriesTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'delay_cache_tags_invalidation',
'entity_test',
'system',
'user',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('entity_test');
$this->installEntitySchema('user');
// Ensure the cachetags table already exists.
Cache::invalidateTags([$this->randomString()]);
}
/**
* {@inheritdoc}
*/
public function register(ContainerBuilder $container): void {
parent::register($container);
$container->register('serializer', PhpSerialize::class);
// Register a database cache backend rather than memory-based.
$container->register('cache_factory', DatabaseBackendFactory::class)
->addArgument(new Reference('database'))
->addArgument(new Reference('cache_tags.invalidator.checksum'))
->addArgument(new Reference('settings'))
->addArgument(new Reference('serializer'))
->addArgument(new Reference(TimeInterface::class));
}
/**
* Tests an entity save.
*/
public function testEntitySave(): void {
\Drupal::cache()->set('test_cache_pre-transaction_foobar', 'something', Cache::PERMANENT, ['foobar']);
\Drupal::cache()->set('test_cache_pre-transaction_entity_test_list', 'something', Cache::PERMANENT, ['entity_test_list']);
$entity = EntityTest::create(['name' => $this->randomString()]);
Database::startLog('testEntitySave');
$entity->save();
// Entity save should have deferred cache invalidation to after transaction
// completion for the "entity_test_list", "entity_test_list:entity_test"
// and "4xx-response" tags. Since cache invalidation is a MERGE database
// operation, and in core drivers each MERGE is split in two SELECT and
// INSERT|UPDATE operations, we expect the last 6 logged database queries
// to be related to the {cachetags} table.
$expected_tail_length = 6;
$executed_statements = [];
foreach (Database::getLog('testEntitySave') as $log) {
// Exclude transaction related statements from the log.
if (
str_starts_with($log['query'], 'ROLLBACK TO SAVEPOINT ') ||
str_starts_with($log['query'], 'RELEASE SAVEPOINT ') ||
str_starts_with($log['query'], 'SAVEPOINT ')
) {
continue;
}
$executed_statements[] = $log['query'];
}
$expected_post_transaction_statements = array_keys(array_fill(array_key_last($executed_statements) - $expected_tail_length + 1, $expected_tail_length, TRUE));
$cachetag_statements = $this->getStatementsForTable($executed_statements, 'cachetags');
$tail_cachetag_statements = array_keys(array_slice($cachetag_statements, count($cachetag_statements) - $expected_tail_length, $expected_tail_length, TRUE));
$this->assertSame($expected_post_transaction_statements, $tail_cachetag_statements);
// Verify that a nested entity save occurred.
$this->assertSame('john doe', User::load(1)->getAccountName());
// Cache reads occurring during a transaction that DO NOT depend on
// invalidated cache tags result in cache HITs. Similarly, cache writes that
// DO NOT depend on invalidated cache tags DO get written. Of course, if we
// read either one now, outside of the context of the transaction, we expect
// the same.
$this->assertNotEmpty(\Drupal::state()->get('delay_cache_tags_invalidation_entity_test_insert__pre-transaction_foobar'));
$this->assertNotEmpty(\Drupal::cache()->get('delay_cache_tags_invalidation_entity_test_insert__during_transaction_foobar'));
$this->assertNotEmpty(\Drupal::state()->get('delay_cache_tags_invalidation_user_insert__during_transaction_foobar'));
$this->assertNotEmpty(\Drupal::cache()->get('test_cache_pre-transaction_foobar'));
// Cache reads occurring during a transaction that DO depend on invalidated
// cache tags result in cache MISSes. Similarly, cache writes that DO depend
// on invalidated cache tags DO NOT get written. Of course, if we read
// either one now, outside of the context of the transaction, we expect the
// same.
$this->assertFalse(\Drupal::state()->get('delay_cache_tags_invalidation_entity_test_insert__pre-transaction_entity_test_list'));
$this->assertFalse(\Drupal::cache()->get('delay_cache_tags_invalidation_entity_test_insert__during_transaction_entity_test_list'));
$this->assertFalse(\Drupal::state()->get('delay_cache_tags_invalidation_user_insert__during_transaction_entity_test_list'));
$this->assertFalse(\Drupal::cache()->get('test_cache_pre-transaction_entity_test_list'));
}
/**
* Tests an entity save rollback.
*/
public function testEntitySaveRollback(): void {
\Drupal::cache()
->set('test_cache_pre-transaction_entity_test_list', 'something', Cache::PERMANENT, ['entity_test_list']);
\Drupal::cache()
->set('test_cache_pre-transaction_user_list', 'something', Cache::PERMANENT, ['user_list']);
\Drupal::state()->set('delay_cache_tags_invalidation_exception', TRUE);
try {
EntityTest::create(['name' => $this->randomString()])->save();
$this->fail('Exception not thrown');
}
catch (\Exception $e) {
$this->assertEquals('Abort entity save to trigger transaction rollback.', $e->getMessage());
}
// The cache has not been invalidated.
$this->assertNotEmpty(\Drupal::cache()->get('test_cache_pre-transaction_entity_test_list'));
$this->assertNotEmpty(\Drupal::cache()->get('test_cache_pre-transaction_user_list'));
// Save a user, that should invalidate the cache tagged with user_list but
// not the one with entity_test_list.
User::create([
'name' => 'john doe',
'status' => 1,
])->save();
$this->assertNotEmpty(\Drupal::cache()->get('test_cache_pre-transaction_entity_test_list'));
$this->assertFalse(\Drupal::cache()->get('test_cache_pre-transaction_user_list'));
}
/**
* Filters statements by table name.
*
* @param string[] $statements
* A list of query statements.
* @param string $table_name
* The name of the table to filter by.
*
* @return string[]
* Filtered statement list.
*/
protected function getStatementsForTable(array $statements, $table_name): array {
return array_filter($statements, function ($statement) use ($table_name) {
return $this->isStatementRelatedToTable($statement, $table_name);
});
}
/**
* Determines if a statement is relative to a specified table.
*
* Non-core database drivers can override this method if they have different
* patterns to identify table related statements.
*
* @param string $statement
* The query statement.
* @param string $tableName
* The table name, Drupal style, without curly brackets or prefix.
*
* @return bool
* TRUE if the statement is relative to the table, FALSE otherwise.
*/
protected static function isStatementRelatedToTable(string $statement, string $tableName): bool {
$realTableIdentifier = Database::getConnection()->prefixTables('{' . $tableName . '}');
$pattern = '/.*(INTO|FROM|UPDATE)( |\n)' . preg_quote($realTableIdentifier, '/') . '/';
return preg_match($pattern, $statement) === 1 ? TRUE : FALSE;
}
}

View File

@ -0,0 +1,690 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Cache;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests any cache backend.
*
* Full generic unit test suite for any cache backend. In order to use it for a
* cache backend implementation, extend this class and override the
* createBackendInstance() method to return an object.
*
* @see DatabaseBackendUnitTestCase
* For a full working implementation.
*/
abstract class GenericCacheBackendUnitTestBase extends KernelTestBase {
/**
* Array of objects implementing Drupal\Core\Cache\CacheBackendInterface.
*
* @var array
*/
protected $cacheBackends;
/**
* Cache bin to use for testing.
*
* @var string
*/
protected $testBin;
/**
* Random value to use in tests.
*
* @var string
*/
protected $defaultValue;
/**
* Most cache backends ensure changes to objects do not affect the cache.
*
* Some caches explicitly allow this, for example,
* \Drupal\Core\Cache\MemoryCache\MemoryCache.
*
* @var bool
*/
protected bool $testObjectProperties = TRUE;
/**
* Gets the testing bin.
*
* Override this method if you want to work on a different bin than the
* default one.
*
* @return string
* Bin name.
*/
protected function getTestBin() {
if (!isset($this->testBin)) {
$this->testBin = 'page';
}
return $this->testBin;
}
/**
* Creates a cache backend to test.
*
* Override this method to test a CacheBackend.
*
* @param string $bin
* Bin name to use for this backend instance.
*
* @return \Drupal\Core\Cache\CacheBackendInterface
* Cache backend to test.
*/
abstract protected function createCacheBackend($bin);
/**
* Allows specific implementation to change the environment before a test run.
*/
public function setUpCacheBackend() {
}
/**
* Allows alteration of environment after a test run but before tear down.
*
* Used before the real tear down because the tear down will change things
* such as the database prefix.
*/
public function tearDownCacheBackend() {
}
/**
* Gets a backend to test; this will get a shared instance set in the object.
*
* @return \Drupal\Core\Cache\CacheBackendInterface
* Cache backend to test.
*/
protected function getCacheBackend($bin = NULL) {
if (!isset($bin)) {
$bin = $this->getTestBin();
}
if (!isset($this->cacheBackends[$bin])) {
$this->cacheBackends[$bin] = $this->createCacheBackend($bin);
// Ensure the backend is empty.
$this->cacheBackends[$bin]->deleteAll();
}
return $this->cacheBackends[$bin];
}
/**
* {@inheritdoc}
*/
protected function setUp(): void {
$this->cacheBackends = [];
$this->defaultValue = $this->randomMachineName(10);
parent::setUp();
$this->setUpCacheBackend();
}
/**
* {@inheritdoc}
*/
protected function tearDown(): void {
// Destruct the registered backend, each test will get a fresh instance,
// properly emptying it here ensure that on persistent data backends they
// will come up empty the next test.
foreach ($this->cacheBackends as $bin => $cache_backend) {
$this->cacheBackends[$bin]->deleteAll();
}
unset($this->cacheBackends);
$this->tearDownCacheBackend();
parent::tearDown();
}
/**
* Tests the get and set methods of Drupal\Core\Cache\CacheBackendInterface.
*/
public function testSetGet(): void {
$backend = $this->getCacheBackend();
$with_backslash = ['foo' => '\Drupal\foo\Bar'];
$backend->set('test1', $with_backslash);
$cached = $backend->get('test1');
$this->assertIsObject($cached);
$this->assertSame($with_backslash, $cached->data);
$this->assertTrue($cached->valid, 'Item is marked as valid.');
// We need to round because microtime may be rounded up in the backend.
$this->assertGreaterThanOrEqual(\Drupal::time()->getRequestTime(), $cached->created);
$this->assertLessThanOrEqual(round(microtime(TRUE), 3), $cached->created);
$this->assertEquals(Cache::PERMANENT, $cached->expire, 'Expire time is correct.');
$backend->set('test2', ['value' => 3], \Drupal::time()->getRequestTime() + 3);
$cached = $backend->get('test2');
$this->assertIsObject($cached);
$this->assertSame(['value' => 3], $cached->data);
$this->assertTrue($cached->valid, 'Item is marked as valid.');
$this->assertGreaterThanOrEqual(\Drupal::time()->getRequestTime(), $cached->created);
$this->assertLessThanOrEqual(round(microtime(TRUE), 3), $cached->created);
$this->assertEquals(\Drupal::time()->getRequestTime() + 3, $cached->expire, 'Expire time is correct.');
$backend->set('test3', 'foobar', \Drupal::time()->getRequestTime() - 3);
$this->assertFalse($backend->get('test3'), 'Invalid item not returned.');
$cached = $backend->get('test3', TRUE);
$this->assertIsObject($cached);
$this->assertFalse($cached->valid, 'Item is marked as valid.');
$this->assertGreaterThanOrEqual(\Drupal::time()->getRequestTime(), $cached->created);
$this->assertLessThanOrEqual(round(microtime(TRUE), 3), $cached->created);
$this->assertEquals(\Drupal::time()->getRequestTime() - 3, $cached->expire, 'Expire time is correct.');
$with_eof = ['foo' => "\nEOF\ndata"];
$backend->set('test4', $with_eof);
$cached = $backend->get('test4');
$this->assertIsObject($cached);
$this->assertSame($with_eof, $cached->data);
$this->assertTrue($cached->valid, 'Item is marked as valid.');
$this->assertGreaterThanOrEqual(\Drupal::time()->getRequestTime(), $cached->created);
$this->assertLessThanOrEqual(round(microtime(TRUE), 3), $cached->created);
$this->assertEquals(Cache::PERMANENT, $cached->expire, 'Expire time is correct.');
$with_eof_and_semicolon = ['foo' => "\nEOF;\ndata"];
$backend->set('test5', $with_eof_and_semicolon);
$cached = $backend->get('test5');
$this->assertIsObject($cached);
$this->assertSame($with_eof_and_semicolon, $cached->data);
$this->assertTrue($cached->valid, 'Item is marked as valid.');
$this->assertGreaterThanOrEqual(\Drupal::time()->getRequestTime(), $cached->created);
$this->assertLessThanOrEqual(round(microtime(TRUE), 3), $cached->created);
$this->assertEquals(Cache::PERMANENT, $cached->expire, 'Expire time is correct.');
$with_variable = ['foo' => '$bar'];
$backend->set('test6', $with_variable);
$cached = $backend->get('test6');
$this->assertIsObject($cached);
$this->assertSame($with_variable, $cached->data);
// Make sure that a cached object is not affected by changing the original.
$data = new \stdClass();
$data->value = 1;
$data->obj = new \stdClass();
$data->obj->value = 2;
$backend->set('test7', $data);
$expected_data = clone $data;
// Add a property to the original. It should not appear in the cached data.
$data->this_should_not_be_in_the_cache = TRUE;
$cached = $backend->get('test7');
$this->assertIsObject($cached);
if ($this->testObjectProperties) {
$this->assertEquals($expected_data, $cached->data);
$this->assertFalse(isset($cached->data->this_should_not_be_in_the_cache));
// Add a property to the cache data. It should not appear when we fetch
// the data from cache again.
$cached->data->this_should_not_be_in_the_cache = TRUE;
$fresh_cached = $backend->get('test7');
$this->assertFalse(isset($fresh_cached->data->this_should_not_be_in_the_cache));
}
else {
$this->assertSame($data, $cached->data);
}
// Check with a long key.
$cid = str_repeat('a', 300);
$backend->set($cid, 'test');
$this->assertEquals('test', $backend->get($cid)->data);
// Check that the cache key is case sensitive.
$backend->set('TEST8', 'value');
$this->assertEquals('value', $backend->get('TEST8')->data);
$this->assertFalse($backend->get('test8'));
// Test a cid with and without a trailing space is treated as two different
// IDs.
$cid_nospace = 'trailing-space-test';
$backend->set($cid_nospace, $cid_nospace);
$this->assertSame($cid_nospace, $backend->get($cid_nospace)->data);
$this->assertFalse($backend->get($cid_nospace . ' '));
// Calling ::set() with invalid cache tags. This should fail an assertion.
try {
$backend->set('assertion_test', 'value', Cache::PERMANENT, ['node' => [3, 5, 7]]);
$this->fail('::set() was called with invalid cache tags, but runtime assertion did not fail.');
}
catch (\AssertionError) {
// Do nothing; continue testing in extending classes.
}
}
/**
* Tests Drupal\Core\Cache\CacheBackendInterface::delete().
*/
public function testDelete(): void {
$backend = $this->getCacheBackend();
$backend->set('test1', 7);
$this->assertIsObject($backend->get('test1'));
$backend->set('test2', 3);
$this->assertIsObject($backend->get('test2'));
$backend->delete('test1');
$this->assertFalse($backend->get('test1'), "Backend does not contain data for cache id test1 after deletion.");
$this->assertIsObject($backend->get('test2'));
$backend->delete('test2');
$this->assertFalse($backend->get('test2'), "Backend does not contain data for cache id test2 after deletion.");
$long_cid = str_repeat('a', 300);
$backend->set($long_cid, 'test');
$backend->delete($long_cid);
$this->assertFalse($backend->get($long_cid), "Backend does not contain data for long cache id after deletion.");
}
/**
* Tests data type preservation.
*/
public function testValueTypeIsKept(): void {
$backend = $this->getCacheBackend();
$variables = [
'test1' => 1,
'test2' => '0',
'test3' => '',
'test4' => 12.64,
'test5' => FALSE,
'test6' => [1, 2, 3],
];
// Create cache entries.
foreach ($variables as $cid => $data) {
$backend->set($cid, $data);
}
// Retrieve and test cache objects.
foreach ($variables as $cid => $value) {
$object = $backend->get($cid);
$this->assertIsObject($object, sprintf("Backend returned an object for cache id %s.", $cid));
$this->assertSame($value, $object->data, sprintf("Data of cached id %s kept is identical in type and value", $cid));
}
}
/**
* Tests Drupal\Core\Cache\CacheBackendInterface::getMultiple().
*/
public function testGetMultiple(): void {
$backend = $this->getCacheBackend();
// Set numerous testing keys.
$long_cid = str_repeat('a', 300);
$backend->set('test1', 1);
$backend->set('test2', 3);
$backend->set('test3', 5);
$backend->set('test4', 7);
$backend->set('test5', 11);
$backend->set('test6', 13);
$backend->set('test7', 17);
$backend->set($long_cid, 300);
// Mismatch order for harder testing.
$reference = [
'test3',
'test7',
// Cid does not exist.
'test21',
'test6',
// Cid does not exist until added before second getMultiple().
'test19',
'test2',
];
$cids = $reference;
$ret = $backend->getMultiple($cids);
// Test return - ensure it contains existing cache ids.
$this->assertArrayHasKey('test2', $ret, "Existing cache id test2 is set.");
$this->assertArrayHasKey('test3', $ret, "Existing cache id test3 is set.");
$this->assertArrayHasKey('test6', $ret, "Existing cache id test6 is set.");
$this->assertArrayHasKey('test7', $ret, "Existing cache id test7 is set.");
// Test return - ensure that objects has expected properties.
$this->assertTrue($ret['test2']->valid, 'Item is marked as valid.');
$this->assertGreaterThanOrEqual(\Drupal::time()->getRequestTime(), $ret['test2']->created);
$this->assertLessThanOrEqual(round(microtime(TRUE), 3), $ret['test2']->created);
$this->assertEquals(Cache::PERMANENT, $ret['test2']->expire, 'Expire time is correct.');
// Test return - ensure it does not contain nonexistent cache ids.
$this->assertFalse(isset($ret['test19']), "Nonexistent cache id test19 is not set.");
$this->assertFalse(isset($ret['test21']), "Nonexistent cache id test21 is not set.");
// Test values.
$this->assertSame(3, $ret['test2']->data, "Existing cache id test2 has the correct value.");
$this->assertSame(5, $ret['test3']->data, "Existing cache id test3 has the correct value.");
$this->assertSame(13, $ret['test6']->data, "Existing cache id test6 has the correct value.");
$this->assertSame(17, $ret['test7']->data, "Existing cache id test7 has the correct value.");
// Test $cids array - ensure it contains cache id's that do not exist.
$this->assertContains('test19', $cids, "Nonexistent cache id test19 is in cids array.");
$this->assertContains('test21', $cids, "Nonexistent cache id test21 is in cids array.");
// Test $cids array - ensure it does not contain cache id's that exist.
$this->assertNotContains('test2', $cids, "Existing cache id test2 is not in cids array.");
$this->assertNotContains('test3', $cids, "Existing cache id test3 is not in cids array.");
$this->assertNotContains('test6', $cids, "Existing cache id test6 is not in cids array.");
$this->assertNotContains('test7', $cids, "Existing cache id test7 is not in cids array.");
// Test a second time after deleting and setting new keys which ensures that
// if the backend uses statics it does not cause unexpected results.
$backend->delete('test3');
$backend->delete('test6');
$backend->set('test19', 57);
$cids = $reference;
$ret = $backend->getMultiple($cids);
// Test return - ensure it contains existing cache ids.
$this->assertArrayHasKey('test2', $ret, "Existing cache id test2 is set");
$this->assertArrayHasKey('test7', $ret, "Existing cache id test7 is set");
$this->assertArrayHasKey('test19', $ret, "Added cache id test19 is set");
// Test return - ensure it does not contain nonexistent cache ids.
$this->assertFalse(isset($ret['test3']), "Deleted cache id test3 is not set");
$this->assertFalse(isset($ret['test6']), "Deleted cache id test6 is not set");
$this->assertFalse(isset($ret['test21']), "Nonexistent cache id test21 is not set");
// Test values.
$this->assertSame(3, $ret['test2']->data, "Existing cache id test2 has the correct value.");
$this->assertSame(17, $ret['test7']->data, "Existing cache id test7 has the correct value.");
$this->assertSame(57, $ret['test19']->data, "Added cache id test19 has the correct value.");
// Test $cids array - ensure it contains cache id's that do not exist.
$this->assertContains('test3', $cids, "Deleted cache id test3 is in cids array.");
$this->assertContains('test6', $cids, "Deleted cache id test6 is in cids array.");
$this->assertContains('test21', $cids, "Nonexistent cache id test21 is in cids array.");
// Test $cids array - ensure it does not contain cache id's that exist.
$this->assertNotContains('test2', $cids, "Existing cache id test2 is not in cids array.");
$this->assertNotContains('test7', $cids, "Existing cache id test7 is not in cids array.");
$this->assertNotContains('test19', $cids, "Added cache id test19 is not in cids array.");
// Test with a long $cid and non-numeric array key.
$cids = ['key:key' => $long_cid];
$return = $backend->getMultiple($cids);
$this->assertEquals(300, $return[$long_cid]->data);
$this->assertEmpty($cids);
}
/**
* Tests \Drupal\Core\Cache\CacheBackendInterface::setMultiple().
*/
public function testSetMultiple(): void {
$backend = $this->getCacheBackend();
$future_expiration = \Drupal::time()->getRequestTime() + 100;
// Set multiple testing keys.
$backend->set('cid_1', 'Some other value');
$items = [
'cid_1' => ['data' => 1],
'cid_2' => ['data' => 2],
'cid_3' => ['data' => [1, 2]],
'cid_4' => ['data' => 1, 'expire' => $future_expiration],
'cid_5' => ['data' => 1, 'tags' => ['test:a', 'test:b']],
];
$backend->setMultiple($items);
$cids = array_keys($items);
$cached = $backend->getMultiple($cids);
$this->assertEquals($items['cid_1']['data'], $cached['cid_1']->data, 'Over-written cache item set correctly.');
$this->assertTrue($cached['cid_1']->valid, 'Item is marked as valid.');
$this->assertGreaterThanOrEqual(\Drupal::time()->getRequestTime(), $cached['cid_1']->created);
$this->assertLessThanOrEqual(round(microtime(TRUE), 3), $cached['cid_1']->created);
$this->assertEquals(CacheBackendInterface::CACHE_PERMANENT, $cached['cid_1']->expire, 'Cache expiration defaults to permanent.');
$this->assertEquals($items['cid_2']['data'], $cached['cid_2']->data, 'New cache item set correctly.');
$this->assertEquals(CacheBackendInterface::CACHE_PERMANENT, $cached['cid_2']->expire, 'Cache expiration defaults to permanent.');
$this->assertEquals($items['cid_3']['data'], $cached['cid_3']->data, 'New cache item with serialized data set correctly.');
$this->assertEquals(CacheBackendInterface::CACHE_PERMANENT, $cached['cid_3']->expire, 'Cache expiration defaults to permanent.');
$this->assertEquals($items['cid_4']['data'], $cached['cid_4']->data, 'New cache item set correctly.');
$this->assertEquals($future_expiration, $cached['cid_4']->expire, 'Cache expiration has been correctly set.');
$this->assertEquals($items['cid_5']['data'], $cached['cid_5']->data, 'New cache item set correctly.');
// Calling ::setMultiple() with invalid cache tags. This should fail an
// assertion.
try {
$items = [
'exception_test_1' => ['data' => 1, 'tags' => []],
'exception_test_2' => ['data' => 2, 'tags' => ['valid']],
'exception_test_3' => ['data' => 3, 'tags' => ['node' => [3, 5, 7]]],
];
$backend->setMultiple($items);
$this->fail('::setMultiple() was called with invalid cache tags, but runtime assertion did not fail.');
}
catch (\AssertionError) {
// Do nothing; continue testing in extending classes.
}
}
/**
* @covers \Drupal\Core\Cache\ApcuBackend::deleteMultiple
* @covers \Drupal\Core\Cache\BackendChain::deleteMultiple
* @covers \Drupal\Core\Cache\ChainedFastBackend::deleteMultiple
* @covers \Drupal\Core\Cache\DatabaseBackend::deleteMultiple
* @covers \Drupal\Core\Cache\MemoryBackend::deleteMultiple
* @covers \Drupal\Core\Cache\PhpBackend::deleteMultiple
* @covers \Drupal\Core\Cache\ApcuBackend::deleteMultiple
* @covers \Drupal\Core\Cache\BackendChain::deleteMultiple
* @covers \Drupal\Core\Cache\ChainedFastBackend::deleteMultiple
* @covers \Drupal\Core\Cache\DatabaseBackend::deleteMultiple
* @covers \Drupal\Core\Cache\MemoryBackend::deleteMultiple
* @covers \Drupal\Core\Cache\PhpBackend::deleteMultiple
*/
public function testDeleteMultiple(): void {
$backend = $this->getCacheBackend();
// Set numerous testing keys.
$backend->set('test1', 1);
$backend->set('test2', 3);
$backend->set('test3', 5);
$backend->set('test4', 7);
$backend->set('test5', 11);
$backend->set('test6', 13);
$backend->set('test7', 17);
$backend->delete('test1');
// Nonexistent key should not cause an error.
$backend->delete('test23');
$backend->deleteMultiple([
'test3',
'test5',
'test7',
// Nonexistent key should not cause an error.
'test19',
// Nonexistent key should not cause an error.
'test21',
]);
// Test if expected keys have been deleted.
$this->assertFalse($backend->get('test1'), "Cache id test1 deleted.");
$this->assertFalse($backend->get('test3'), "Cache id test3 deleted.");
$this->assertFalse($backend->get('test5'), "Cache id test5 deleted.");
$this->assertFalse($backend->get('test7'), "Cache id test7 deleted.");
// Test if expected keys exist.
$this->assertNotFalse($backend->get('test2'), "Cache id test2 exists.");
$this->assertNotFalse($backend->get('test4'), "Cache id test4 exists.");
$this->assertNotFalse($backend->get('test6'), "Cache id test6 exists.");
// Test if that expected keys do not exist.
$this->assertFalse($backend->get('test19'), "Cache id test19 does not exist.");
$this->assertFalse($backend->get('test21'), "Cache id test21 does not exist.");
// Calling deleteMultiple() with an empty array should not cause an error.
$this->assertNull($backend->deleteMultiple([]));
}
/**
* Tests Drupal\Core\Cache\CacheBackendInterface::deleteAll().
*/
public function testDeleteAll(): void {
$backend_a = $this->getCacheBackend();
$backend_b = $this->getCacheBackend('bootstrap');
// Set both expiring and permanent keys.
$backend_a->set('test1', 1, Cache::PERMANENT);
$backend_a->set('test2', 3, time() + 1000);
$backend_b->set('test3', 4, Cache::PERMANENT);
$backend_a->deleteAll();
$this->assertFalse($backend_a->get('test1'), 'First key has been deleted.');
$this->assertFalse($backend_a->get('test2'), 'Second key has been deleted.');
$this->assertNotEmpty($backend_b->get('test3'), 'Item in other bin is preserved.');
}
/**
* @covers \Drupal\Core\Cache\ApcuBackend::getMultiple
* @covers \Drupal\Core\Cache\BackendChain::getMultiple
* @covers \Drupal\Core\Cache\ChainedFastBackend::getMultiple
* @covers \Drupal\Core\Cache\DatabaseBackend::getMultiple
* @covers \Drupal\Core\Cache\MemoryBackend::getMultiple
* @covers \Drupal\Core\Cache\PhpBackend::getMultiple
* @covers \Drupal\Core\Cache\ApcuBackend::invalidateMultiple
* @covers \Drupal\Core\Cache\BackendChain::invalidateMultiple
* @covers \Drupal\Core\Cache\ChainedFastBackend::invalidateMultiple
* @covers \Drupal\Core\Cache\DatabaseBackend::invalidateMultiple
* @covers \Drupal\Core\Cache\MemoryBackend::invalidateMultiple
* @covers \Drupal\Core\Cache\PhpBackend::invalidateMultiple
*/
public function testInvalidate(): void {
$backend = $this->getCacheBackend();
$backend->set('test1', 1);
$backend->set('test2', 2);
$backend->set('test3', 2);
$backend->set('test4', 2);
$reference = ['test1', 'test2', 'test3', 'test4'];
$cids = $reference;
$ret = $backend->getMultiple($cids);
$this->assertCount(4, $ret, 'Four items returned.');
$backend->invalidate('test1');
$backend->invalidateMultiple(['test2', 'test3']);
$cids = $reference;
$ret = $backend->getMultiple($cids);
$this->assertCount(1, $ret, 'Only one item element returned.');
$cids = $reference;
$ret = $backend->getMultiple($cids, TRUE);
$this->assertCount(4, $ret, 'Four items returned.');
// Calling invalidateMultiple() with an empty array should not cause an
// error.
$this->assertNull($backend->invalidateMultiple([]));
}
/**
* Tests Drupal\Core\Cache\CacheBackendInterface::invalidateTags().
*/
public function testInvalidateTags(): void {
$backend = $this->getCacheBackend();
// Create two cache entries with the same tag and tag value.
$backend->set('test_cid_invalidate1', $this->defaultValue, Cache::PERMANENT, ['test_tag:2']);
$backend->set('test_cid_invalidate2', $this->defaultValue, Cache::PERMANENT, ['test_tag:2']);
$this->assertSame($this->defaultValue, $backend->get('test_cid_invalidate1')->data);
$this->assertSame($this->defaultValue, $backend->get('test_cid_invalidate2')->data);
// Invalidate test_tag of value 1. This should invalidate both entries.
Cache::invalidateTags(['test_tag:2']);
$this->assertFalse($backend->get('test_cid_invalidate1') || $backend->get('test_cid_invalidate2'), 'Two cache items invalidated after invalidating a cache tag.');
// Verify that cache items have not been deleted after invalidation.
$this->assertSame($this->defaultValue, $backend->get('test_cid_invalidate1', TRUE)->data);
$this->assertSame($this->defaultValue, $backend->get('test_cid_invalidate2', TRUE)->data);
// Create two cache entries with the same tag and an array tag value.
$backend->set('test_cid_invalidate1', $this->defaultValue, Cache::PERMANENT, ['test_tag:1']);
$backend->set('test_cid_invalidate2', $this->defaultValue, Cache::PERMANENT, ['test_tag:1']);
$this->assertSame($this->defaultValue, $backend->get('test_cid_invalidate1')->data);
$this->assertSame($this->defaultValue, $backend->get('test_cid_invalidate2')->data);
// Invalidate test_tag of value 1. This should invalidate both entries.
Cache::invalidateTags(['test_tag:1']);
$this->assertFalse($backend->get('test_cid_invalidate1') || $backend->get('test_cid_invalidate2'), 'Two caches removed after invalidating a cache tag.');
// Verify that cache items have not been deleted after invalidation.
$this->assertSame($this->defaultValue, $backend->get('test_cid_invalidate1', TRUE)->data);
$this->assertSame($this->defaultValue, $backend->get('test_cid_invalidate2', TRUE)->data);
// Create three cache entries with a mix of tags and tag values.
$backend->set('test_cid_invalidate1', $this->defaultValue, Cache::PERMANENT, ['test_tag:1']);
$backend->set('test_cid_invalidate2', $this->defaultValue, Cache::PERMANENT, ['test_tag:2']);
$backend->set('test_cid_invalidate3', $this->defaultValue, Cache::PERMANENT, ['test_tag_foo:3']);
$this->assertSame($this->defaultValue, $backend->get('test_cid_invalidate1')->data);
$this->assertSame($this->defaultValue, $backend->get('test_cid_invalidate2')->data);
$this->assertSame($this->defaultValue, $backend->get('test_cid_invalidate3')->data);
Cache::invalidateTags(['test_tag_foo:3']);
// Verify that cache items not matching the tag were not invalidated.
$this->assertSame($this->defaultValue, $backend->get('test_cid_invalidate1')->data);
$this->assertSame($this->defaultValue, $backend->get('test_cid_invalidate2')->data);
$this->assertFalse($backend->get('test_cid_invalidate3'), 'Cached item matching the tag was removed.');
// Create cache entry in multiple bins. Two cache entries
// (test_cid_invalidate1 and test_cid_invalidate2) still exist from previous
// tests.
$tags = ['test_tag:1', 'test_tag:2', 'test_tag:3'];
$bins = ['path', 'bootstrap', 'page'];
foreach ($bins as $bin) {
$this->getCacheBackend($bin)->set('test', $this->defaultValue, Cache::PERMANENT, $tags);
$this->assertNotEmpty($this->getCacheBackend($bin)->get('test'), 'Cache item was set in bin.');
}
Cache::invalidateTags(['test_tag:2']);
// Test that the cache entry has been invalidated in multiple bins.
foreach ($bins as $bin) {
$this->assertFalse($this->getCacheBackend($bin)->get('test'), 'Tag invalidation affected item in bin.');
}
// Test that the cache entry with a matching tag has been invalidated.
$this->assertFalse($this->getCacheBackend($bin)->get('test_cid_invalidate2'), 'Cache items matching tag were invalidated.');
// Test that the cache entry with without a matching tag still exists.
$this->assertNotEmpty($this->getCacheBackend($bin)->get('test_cid_invalidate1'), 'Cache items not matching tag were not invalidated.');
}
/**
* Tests Drupal\Core\Cache\CacheBackendInterface::invalidateAll().
*
* @group legacy
*/
public function testInvalidateAll(): void {
$backend_a = $this->getCacheBackend();
$backend_b = $this->getCacheBackend('bootstrap');
// Set both expiring and permanent keys.
$backend_a->set('test1', 1, Cache::PERMANENT);
$backend_a->set('test2', 3, time() + 1000);
$backend_b->set('test3', 4, Cache::PERMANENT);
$this->expectDeprecation('CacheBackendInterface::invalidateAll() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use CacheBackendInterface::deleteAll() or cache tag invalidation instead. See https://www.drupal.org/node/3500622');
$backend_a->invalidateAll();
$this->assertFalse($backend_a->get('test1'), 'First key has been invalidated.');
$this->assertFalse($backend_a->get('test2'), 'Second key has been invalidated.');
$this->assertNotEmpty($backend_b->get('test3'), 'Item in other bin is preserved.');
$this->assertNotEmpty($backend_a->get('test1', TRUE), 'First key has not been deleted.');
$this->assertNotEmpty($backend_a->get('test2', TRUE), 'Second key has not been deleted.');
}
/**
* Tests Drupal\Core\Cache\CacheBackendInterface::removeBin().
*/
public function testRemoveBin(): void {
$backend_a = $this->getCacheBackend();
$backend_b = $this->getCacheBackend('bootstrap');
// Set both expiring and permanent keys.
$backend_a->set('test1', 1, Cache::PERMANENT);
$backend_a->set('test2', 3, time() + 1000);
$backend_b->set('test3', 4, Cache::PERMANENT);
$backend_a->removeBin();
$this->assertFalse($backend_a->get('test1'), 'First key has been deleted.');
$this->assertFalse($backend_a->get('test2', TRUE), 'Second key has been deleted.');
$this->assertNotEmpty($backend_b->get('test3'), 'Item in other bin is preserved.');
}
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Cache;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Cache\MemoryCache\LruMemoryCache;
/**
* Unit test of the LRU memory cache using the generic cache unit test base.
*
* @group Cache
*/
class LruCacheGenericTest extends GenericCacheBackendUnitTestBase {
/**
* {@inheritdoc}
*/
protected bool $testObjectProperties = FALSE;
/**
* Creates a new instance of LruMemoryCache.
*
* @return \Drupal\Core\Cache\CacheBackendInterface
* A new MemoryBackend object.
*/
protected function createCacheBackend($bin) {
$backend = new LruMemoryCache(\Drupal::service(TimeInterface::class), 300);
\Drupal::service('cache_tags.invalidator')->addInvalidator($backend);
return $backend;
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Cache;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Cache\MemoryBackend;
/**
* Unit test of the memory cache backend using the generic cache unit test base.
*
* @group Cache
*/
class MemoryBackendTest extends GenericCacheBackendUnitTestBase {
/**
* Creates a new instance of MemoryBackend.
*
* @return \Drupal\Core\Cache\CacheBackendInterface
* A new MemoryBackend object.
*/
protected function createCacheBackend($bin) {
$backend = new MemoryBackend(\Drupal::service(TimeInterface::class));
\Drupal::service('cache_tags.invalidator')->addInvalidator($backend);
return $backend;
}
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Cache;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Cache\MemoryCache\MemoryCache;
/**
* Unit test of the memory cache using the generic cache unit test base.
*
* @group Cache
*/
class MemoryCacheGenericTest extends GenericCacheBackendUnitTestBase {
/**
* {@inheritdoc}
*/
protected bool $testObjectProperties = FALSE;
/**
* Creates a new instance of MemoryCache.
*
* @return \Drupal\Core\Cache\CacheBackendInterface
* A new MemoryBackend object.
*/
protected function createCacheBackend($bin) {
$backend = new MemoryCache(\Drupal::service(TimeInterface::class));
\Drupal::service('cache_tags.invalidator')->addInvalidator($backend);
return $backend;
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Cache;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Cache\PhpBackend;
/**
* Unit test of the PHP cache backend using the generic cache unit test base.
*
* @group Cache
*/
class PhpBackendTest extends GenericCacheBackendUnitTestBase {
/**
* Creates a new instance of MemoryBackend.
*
* @return \Drupal\Core\Cache\CacheBackendInterface
* A new PhpBackend object.
*/
protected function createCacheBackend($bin) {
return new PhpBackend($bin, \Drupal::service('cache_tags.invalidator.checksum'), \Drupal::service(TimeInterface::class));
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\ClassLoader;
use Drupal\Component\Utility\Random;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\StringTranslation\TranslationWrapper;
use Drupal\KernelTests\KernelTestBase;
use Drupal\module_autoload_test\Foo;
/**
* @coversDefaultClass Drupal\Core\ClassLoader\BackwardsCompatibilityClassLoader
* @group ClassLoader
*/
class BackwardsCompatibilityClassLoaderTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['module_autoload_test'];
/**
* Tests that the bc layer for TranslationWrapper works.
*/
public function testTranslationWrapper(): void {
// @phpstan-ignore class.notFound
$object = new TranslationWrapper('Backward compatibility');
$this->assertInstanceOf(TranslatableMarkup::class, $object);
}
/**
* Tests that a moved class from a module works.
*
* @group legacy
*/
public function testModuleMovedClass(): void {
// @phpstan-ignore class.notFound
$this->expectDeprecation('Class ' . Foo::class . ' is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0, use Drupal\Component\Utility\Random instead. See https://www.drupal.org/project/drupal/issues/3502882');
// @phpstan-ignore class.notFound
$object = new Foo();
$this->assertInstanceOf(Random::class, $object);
}
}

View File

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Common;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\system_test\Hook\SystemTestHooks;
use Drupal\KernelTests\KernelTestBase;
/**
* @covers ::drupal_flush_all_caches
* @group Common
*/
class DrupalFlushAllCachesTest extends KernelTestBase {
/**
* Stores the number of container builds.
*
* @var int
*/
protected $containerBuilds = 0;
/**
* {@inheritdoc}
*/
protected static $modules = ['system'];
/**
* Tests that drupal_flush_all_caches() uses core.extension properly.
*/
public function testDrupalFlushAllCachesModuleList(): void {
$this->assertFalse(function_exists('system_test_help'));
$core_extension = \Drupal::configFactory()->getEditable('core.extension');
$module = $core_extension->get('module');
$module['system_test'] = -10;
$core_extension->set('module', module_config_sort($module))->save();
$this->containerBuilds = 0;
drupal_flush_all_caches();
$module_list = ['system_test', 'system'];
$database_module = \Drupal::database()->getProvider();
if ($database_module !== 'core') {
$module_list[] = $database_module;
}
sort($module_list);
$container_modules = array_keys($this->container->getParameter('container.modules'));
sort($container_modules);
$this->assertSame($module_list, $container_modules);
$this->assertSame(1, $this->containerBuilds);
$this->assertTrue(method_exists(SystemTestHooks::class, 'help'));
$core_extension->clear('module.system_test')->save();
$this->containerBuilds = 0;
drupal_flush_all_caches();
$module_list = ['system'];
if ($database_module !== 'core') {
$module_list[] = $database_module;
}
sort($module_list);
$container_modules = array_keys($this->container->getParameter('container.modules'));
sort($container_modules);
$this->assertSame($module_list, $container_modules);
$this->assertSame(1, $this->containerBuilds);
}
/**
* {@inheritdoc}
*/
public function register(ContainerBuilder $container): void {
parent::register($container);
$this->containerBuilds++;
}
}

View File

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Common;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests XSS filtering.
*
* @see \Drupal\Component\Utility\Xss::filter()
* @see \Drupal\Component\Utility\UrlHelper::filterBadProtocol
* @see \Drupal\Component\Utility\UrlHelper::stripDangerousProtocols
*
* @group Common
*/
class XssUnitTest extends KernelTestBase {
use StringTranslationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['filter', 'system'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(['system']);
}
/**
* Tests t() functionality.
*/
public function testT(): void {
$text = $this->t('Simple text');
$this->assertSame('Simple text', (string) $text, 't leaves simple text alone.');
$text = $this->t('Escaped text: @value', ['@value' => '<script>']);
$this->assertSame('Escaped text: &lt;script&gt;', (string) $text, 't replaces and escapes string.');
$text = $this->t('Placeholder text: %value', ['%value' => '<script>']);
$this->assertSame('Placeholder text: <em class="placeholder">&lt;script&gt;</em>', (string) $text, 't replaces, escapes and themes string.');
}
/**
* Checks that harmful protocols are stripped.
*/
public function testBadProtocolStripping(): void {
// Ensure that check_url() strips out harmful protocols, and encodes for
// HTML.
// Ensure \Drupal\Component\Utility\UrlHelper::stripDangerousProtocols() can
// be used to return a plain-text string stripped of harmful protocols.
$url = 'javascript:http://www.example.com/?x=1&y=2';
$expected_plain = 'http://www.example.com/?x=1&y=2';
$expected_html = 'http://www.example.com/?x=1&amp;y=2';
$this->assertSame($expected_html, UrlHelper::filterBadProtocol($url), '\\Drupal\\Component\\Utility\\UrlHelper::filterBadProtocol() filters a URL and encodes it for HTML.');
$this->assertSame($expected_plain, UrlHelper::stripDangerousProtocols($url), '\\Drupal\\Component\\Utility\\UrlHelper::stripDangerousProtocols() filters a URL and returns plain text.');
}
}

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Condition;
use Drupal\Core\Condition\ConditionPluginCollection;
use Drupal\KernelTests\KernelTestBase;
/**
* @coversDefaultClass \Drupal\Core\Condition\ConditionPluginCollection
*
* @group Condition
*/
class ConditionPluginCollectionTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'user',
'path_alias',
];
/**
* @covers ::getConfiguration
*/
public function testGetConfiguration(): void {
// Include a condition that has custom configuration and a type mismatch on
// 'negate' by using 0 instead of FALSE.
$configuration['request_path'] = [
'id' => 'request_path',
'negate' => 0,
'context_mapping' => [],
'pages' => '/user/*',
];
// Include a condition that matches default values but with a type mismatch
// on 'negate' by using 0 instead of FALSE. This condition will be removed,
// because condition configurations that match default values with "=="
// comparison are not saved or exported.
$configuration['user_role'] = [
'id' => 'user_role',
'negate' => '0',
'context_mapping' => [],
'roles' => [],
];
$collection = new ConditionPluginCollection(\Drupal::service('plugin.manager.condition'), $configuration);
$expected['request_path'] = [
'id' => 'request_path',
'negate' => 0,
'context_mapping' => [],
'pages' => '/user/*',
];
// NB: The 'user_role' property should not exist in expected set.
$this->assertSame($expected, $collection->getConfiguration());
}
}

View File

@ -0,0 +1,324 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config\Action;
// cspell:ignore inflector
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Component\Uuid\Uuid;
use Drupal\config_test\ConfigActionErrorEntity\DuplicatePluralizedMethodName;
use Drupal\config_test\ConfigActionErrorEntity\DuplicatePluralizedOtherMethodName;
use Drupal\Core\Config\Action\ConfigActionException;
use Drupal\Core\Config\Action\DuplicateConfigActionIdException;
use Drupal\Core\Config\Action\EntityMethodException;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the config action system.
*
* @group config
*/
class ConfigActionTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['config_test'];
/**
* @see \Drupal\Core\Config\Action\Plugin\ConfigAction\EntityCreate
*/
public function testEntityCreate(): void {
$this->assertCount(0, \Drupal::entityTypeManager()->getStorage('config_test')->loadMultiple(), 'There are no config_test entities');
/** @var \Drupal\Core\Config\Action\ConfigActionManager $manager */
$manager = $this->container->get('plugin.manager.config_action');
$manager->applyAction('entity_create:createIfNotExists', 'config_test.dynamic.action_test', ['label' => 'Action test']);
/** @var \Drupal\config_test\Entity\ConfigTest[] $config_test_entities */
$config_test_entities = \Drupal::entityTypeManager()->getStorage('config_test')->loadMultiple();
$this->assertCount(1, \Drupal::entityTypeManager()->getStorage('config_test')->loadMultiple(), 'There is 1 config_test entity');
$this->assertSame('Action test', $config_test_entities['action_test']->label());
$this->assertTrue(Uuid::isValid((string) $config_test_entities['action_test']->uuid()), 'Config entity assigned a valid UUID');
// Calling createIfNotExists action again will not error.
$manager->applyAction('entity_create:createIfNotExists', 'config_test.dynamic.action_test', ['label' => 'Action test']);
try {
$manager->applyAction('entity_create:create', 'config_test.dynamic.action_test', ['label' => 'Action test']);
$this->fail('Expected exception not thrown');
}
catch (ConfigActionException $e) {
$this->assertSame('Entity config_test.dynamic.action_test exists', $e->getMessage());
}
}
/**
* @see \Drupal\Core\Config\Action\Plugin\ConfigAction\EntityMethod
*/
public function testEntityMethod(): void {
$this->installConfig('config_test');
$storage = \Drupal::entityTypeManager()->getStorage('config_test');
/** @var \Drupal\config_test\Entity\ConfigTest $config_test_entity */
$config_test_entity = $storage->load('dotted.default');
$this->assertSame('Default', $config_test_entity->getProtectedProperty());
/** @var \Drupal\Core\Config\Action\ConfigActionManager $manager */
$manager = $this->container->get('plugin.manager.config_action');
// Call a method action.
$manager->applyAction('entity_method:config_test.dynamic:setProtectedProperty', 'config_test.dynamic.dotted.default', 'Test value');
/** @var \Drupal\config_test\Entity\ConfigTest $config_test_entity */
$config_test_entity = $storage->load('dotted.default');
$this->assertSame('Test value', $config_test_entity->getProtectedProperty());
$manager->applyAction('entity_method:config_test.dynamic:setProtectedProperty', 'config_test.dynamic.dotted.default', 'Test value 2');
/** @var \Drupal\config_test\Entity\ConfigTest $config_test_entity */
$config_test_entity = $storage->load('dotted.default');
$this->assertSame('Test value 2', $config_test_entity->getProtectedProperty());
$manager->applyAction('entity_method:config_test.dynamic:concatProtectedProperty', 'config_test.dynamic.dotted.default', ['Test value ', '3']);
/** @var \Drupal\config_test\Entity\ConfigTest $config_test_entity */
$config_test_entity = $storage->load('dotted.default');
$this->assertSame('Test value 3', $config_test_entity->getProtectedProperty());
$manager->applyAction('entity_method:config_test.dynamic:concatProtectedPropertyOptional', 'config_test.dynamic.dotted.default', ['Test value ', '4']);
/** @var \Drupal\config_test\Entity\ConfigTest $config_test_entity */
$config_test_entity = $storage->load('dotted.default');
$this->assertSame('Test value 4', $config_test_entity->getProtectedProperty());
// Test calling an action that has 2 arguments but one is optional with an
// array value.
$manager->applyAction('entity_method:config_test.dynamic:concatProtectedPropertyOptional', 'config_test.dynamic.dotted.default', ['Test value 5']);
/** @var \Drupal\config_test\Entity\ConfigTest $config_test_entity */
$config_test_entity = $storage->load('dotted.default');
$this->assertSame('Test value 5', $config_test_entity->getProtectedProperty());
// Test calling an action that has 2 arguments but one is optional with a
// non array value.
$manager->applyAction('entity_method:config_test.dynamic:concatProtectedPropertyOptional', 'config_test.dynamic.dotted.default', 'Test value 6');
/** @var \Drupal\config_test\Entity\ConfigTest $config_test_entity */
$config_test_entity = $storage->load('dotted.default');
$this->assertSame('Test value 6', $config_test_entity->getProtectedProperty());
// Test calling an action that expects no arguments.
$manager->applyAction('entity_method:config_test.dynamic:defaultProtectedProperty', 'config_test.dynamic.dotted.default', []);
/** @var \Drupal\config_test\Entity\ConfigTest $config_test_entity */
$config_test_entity = $storage->load('dotted.default');
$this->assertSame('Set by method', $config_test_entity->getProtectedProperty());
$manager->applyAction('entity_method:config_test.dynamic:addToArray', 'config_test.dynamic.dotted.default', 'foo');
$manager->applyAction('entity_method:config_test.dynamic:addToArray', 'config_test.dynamic.dotted.default', 'bar');
/** @var \Drupal\config_test\Entity\ConfigTest $config_test_entity */
$config_test_entity = $storage->load('dotted.default');
$this->assertSame(['foo', 'bar'], $config_test_entity->getArrayProperty());
$manager->applyAction('entity_method:config_test.dynamic:addToArray', 'config_test.dynamic.dotted.default', ['a', 'b', 'c']);
/** @var \Drupal\config_test\Entity\ConfigTest $config_test_entity */
$config_test_entity = $storage->load('dotted.default');
$this->assertSame(['foo', 'bar', ['a', 'b', 'c']], $config_test_entity->getArrayProperty());
$manager->applyAction('entity_method:config_test.dynamic:setArray', 'config_test.dynamic.dotted.default', ['a', 'b', 'c']);
/** @var \Drupal\config_test\Entity\ConfigTest $config_test_entity */
$config_test_entity = $storage->load('dotted.default');
$this->assertSame(['a', 'b', 'c'], $config_test_entity->getArrayProperty());
$manager->applyAction('entity_method:config_test.dynamic:setArray', 'config_test.dynamic.dotted.default', [['a', 'b', 'c'], ['a']]);
/** @var \Drupal\config_test\Entity\ConfigTest $config_test_entity */
$config_test_entity = $storage->load('dotted.default');
$this->assertSame([['a', 'b', 'c'], ['a']], $config_test_entity->getArrayProperty());
$config_test_entity->delete();
try {
$manager->applyAction('entity_method:config_test.dynamic:setProtectedProperty', 'config_test.dynamic.dotted.default', 'Test value');
$this->fail('Expected exception not thrown');
}
catch (ConfigActionException $e) {
$this->assertSame('Entity config_test.dynamic.dotted.default does not exist', $e->getMessage());
}
// Test custom and default admin labels.
$this->assertSame('Test configuration append', (string) $manager->getDefinition('entity_method:config_test.dynamic:append')['admin_label']);
$this->assertSame('Set default name', (string) $manager->getDefinition('entity_method:config_test.dynamic:defaultProtectedProperty')['admin_label']);
}
/**
* @see \Drupal\Core\Config\Action\Plugin\ConfigAction\EntityMethod
*/
public function testPluralizedEntityMethod(): void {
$this->installConfig('config_test');
$storage = \Drupal::entityTypeManager()->getStorage('config_test');
/** @var \Drupal\Core\Config\Action\ConfigActionManager $manager */
$manager = $this->container->get('plugin.manager.config_action');
// Call a pluralized method action.
$manager->applyAction('entity_method:config_test.dynamic:addToArrayMultipleTimes', 'config_test.dynamic.dotted.default', ['a', 'b', 'c', 'd']);
/** @var \Drupal\config_test\Entity\ConfigTest $config_test_entity */
$config_test_entity = $storage->load('dotted.default');
$this->assertSame(['a', 'b', 'c', 'd'], $config_test_entity->getArrayProperty());
$manager->applyAction('entity_method:config_test.dynamic:addToArrayMultipleTimes', 'config_test.dynamic.dotted.default', [['foo'], 'bar']);
/** @var \Drupal\config_test\Entity\ConfigTest $config_test_entity */
$config_test_entity = $storage->load('dotted.default');
$this->assertSame(['a', 'b', 'c', 'd', ['foo'], 'bar'], $config_test_entity->getArrayProperty());
$config_test_entity->setProtectedProperty('')->save();
$manager->applyAction('entity_method:config_test.dynamic:appends', 'config_test.dynamic.dotted.default', ['1', '2', '3']);
/** @var \Drupal\config_test\Entity\ConfigTest $config_test_entity */
$config_test_entity = $storage->load('dotted.default');
$this->assertSame('123', $config_test_entity->getProtectedProperty());
// Test that the inflector converts to a good plural form.
$config_test_entity->setProtectedProperty('')->save();
$manager->applyAction('entity_method:config_test.dynamic:concatProtectedProperties', 'config_test.dynamic.dotted.default', [['1', '2'], ['3', '4']]);
/** @var \Drupal\config_test\Entity\ConfigTest $config_test_entity */
$config_test_entity = $storage->load('dotted.default');
$this->assertSame('34', $config_test_entity->getProtectedProperty());
$this->assertTrue($manager->hasDefinition('entity_method:config_test.dynamic:setProtectedProperty'), 'The setProtectedProperty action exists');
// cspell:ignore Propertys
$this->assertFalse($manager->hasDefinition('entity_method:config_test.dynamic:setProtectedPropertys'), 'There is no automatically pluralized version of the setProtectedProperty action');
// Admin label for pluralized form.
$this->assertSame('Test configuration append (multiple calls)', (string) $manager->getDefinition('entity_method:config_test.dynamic:appends')['admin_label']);
}
/**
* @see \Drupal\Core\Config\Action\Plugin\ConfigAction\EntityMethod
*/
public function testPluralizedEntityMethodException(): void {
$this->installConfig('config_test');
/** @var \Drupal\Core\Config\Action\ConfigActionManager $manager */
$manager = $this->container->get('plugin.manager.config_action');
$this->expectException(EntityMethodException::class);
$this->expectExceptionMessage('The pluralized entity method config action \'entity_method:config_test.dynamic:addToArrayMultipleTimes\' requires an array value in order to call Drupal\config_test\Entity\ConfigTest::addToArray() multiple times');
$manager->applyAction('entity_method:config_test.dynamic:addToArrayMultipleTimes', 'config_test.dynamic.dotted.default', 'Test value');
}
/**
* @see \Drupal\Core\Config\Action\Plugin\ConfigAction\Deriver\EntityMethodDeriver
*/
public function testDuplicatePluralizedMethodNameException(): void {
\Drupal::state()->set('config_test.class_override', DuplicatePluralizedMethodName::class);
\Drupal::entityTypeManager()->clearCachedDefinitions();
$this->installConfig('config_test');
/** @var \Drupal\Core\Config\Action\ConfigActionManager $manager */
$manager = $this->container->get('plugin.manager.config_action');
$this->expectException(EntityMethodException::class);
$this->expectExceptionMessage('Duplicate action can not be created for ID \'config_test.dynamic:testMethod\' for Drupal\config_test\ConfigActionErrorEntity\DuplicatePluralizedMethodName::testMethod(). The existing action is for the ::testMethod() method');
$manager->getDefinitions();
}
/**
* @see \Drupal\Core\Config\Action\Plugin\ConfigAction\Deriver\EntityMethodDeriver
*/
public function testDuplicatePluralizedOtherMethodNameException(): void {
\Drupal::state()->set('config_test.class_override', DuplicatePluralizedOtherMethodName::class);
\Drupal::entityTypeManager()->clearCachedDefinitions();
$this->installConfig('config_test');
/** @var \Drupal\Core\Config\Action\ConfigActionManager $manager */
$manager = $this->container->get('plugin.manager.config_action');
$this->expectException(EntityMethodException::class);
$this->expectExceptionMessage('Duplicate action can not be created for ID \'config_test.dynamic:testMethod2\' for Drupal\config_test\ConfigActionErrorEntity\DuplicatePluralizedOtherMethodName::testMethod2(). The existing action is for the ::testMethod() method');
$manager->getDefinitions();
}
/**
* @see \Drupal\Core\Config\Action\Plugin\ConfigAction\EntityMethod
*/
public function testEntityMethodException(): void {
$this->installConfig('config_test');
/** @var \Drupal\Core\Config\Action\ConfigActionManager $manager */
$manager = $this->container->get('plugin.manager.config_action');
$this->expectException(EntityMethodException::class);
$this->expectExceptionMessage('Entity method config action \'entity_method:config_test.dynamic:concatProtectedProperty\' requires an array value. The number of parameters or required parameters for Drupal\config_test\Entity\ConfigTest::concatProtectedProperty() is not 1');
$manager->applyAction('entity_method:config_test.dynamic:concatProtectedProperty', 'config_test.dynamic.dotted.default', 'Test value');
}
/**
* @see \Drupal\Core\Config\Action\Plugin\ConfigAction\SimpleConfigUpdate
*/
public function testSimpleConfigUpdate(): void {
$this->installConfig('config_test');
$this->assertSame('bar', $this->config('config_test.system')->get('foo'));
/** @var \Drupal\Core\Config\Action\ConfigActionManager $manager */
$manager = $this->container->get('plugin.manager.config_action');
// Call the simple config update action.
$manager->applyAction('simpleConfigUpdate', 'config_test.system', ['foo' => 'Yay!']);
$this->assertSame('Yay!', $this->config('config_test.system')->get('foo'));
try {
$manager->applyAction('simpleConfigUpdate', 'config_test.system', 'Test');
$this->fail('Expected exception not thrown');
}
catch (ConfigActionException $e) {
$this->assertSame('Config config_test.system can not be updated because $value is not an array', $e->getMessage());
}
$this->config('config_test.system')->delete();
try {
$manager->applyAction('simpleConfigUpdate', 'config_test.system', ['foo' => 'Yay!']);
$this->fail('Expected exception not thrown');
}
catch (ConfigActionException $e) {
$this->assertSame('Config config_test.system does not exist so can not be updated', $e->getMessage());
}
}
/**
* @see \Drupal\Core\Config\Action\ConfigActionManager::getShorthandActionIdsForEntityType()
*/
public function testShorthandActionIds(): void {
$storage = \Drupal::entityTypeManager()->getStorage('config_test');
$this->assertCount(0, $storage->loadMultiple(), 'There are no config_test entities');
/** @var \Drupal\Core\Config\Action\ConfigActionManager $manager */
$manager = $this->container->get('plugin.manager.config_action');
$manager->applyAction('createIfNotExists', 'config_test.dynamic.action_test', ['label' => 'Action test', 'protected_property' => '']);
/** @var \Drupal\config_test\Entity\ConfigTest[] $config_test_entities */
$config_test_entities = $storage->loadMultiple();
$this->assertCount(1, $config_test_entities, 'There is 1 config_test entity');
$this->assertSame('Action test', $config_test_entities['action_test']->label());
$this->assertSame('', $config_test_entities['action_test']->getProtectedProperty());
/** @var \Drupal\Core\Config\Action\ConfigActionManager $manager */
$manager = $this->container->get('plugin.manager.config_action');
// Call a method action.
$manager->applyAction('setProtectedProperty', 'config_test.dynamic.action_test', 'Test value');
/** @var \Drupal\config_test\Entity\ConfigTest $config_test_entity */
$config_test_entity = $storage->load('action_test');
$this->assertSame('Test value', $config_test_entity->getProtectedProperty());
}
/**
* @see \Drupal\Core\Config\Action\ConfigActionManager::getShorthandActionIdsForEntityType()
*/
public function testDuplicateShorthandActionIds(): void {
$this->enableModules(['config_action_duplicate_test']);
/** @var \Drupal\Core\Config\Action\ConfigActionManager $manager */
$manager = $this->container->get('plugin.manager.config_action');
$this->expectException(DuplicateConfigActionIdException::class);
$this->expectExceptionMessage("The plugins 'entity_method:config_test.dynamic:setProtectedProperty' and 'config_action_duplicate_test:config_test.dynamic:setProtectedProperty' both resolve to the same shorthand action ID for the 'config_test' entity type");
$manager->applyAction('createIfNotExists', 'config_test.dynamic.action_test', ['label' => 'Action test', 'protected_property' => '']);
}
/**
* @see \Drupal\Core\Config\Action\ConfigActionManager::getShorthandActionIdsForEntityType()
*/
public function testParentAttributes(): void {
$definitions = $this->container->get('plugin.manager.config_action')->getDefinitions();
// The \Drupal\config_test\Entity\ConfigQueryTest::concatProtectedProperty()
// does not have an attribute but the parent does so this is discovered.
$this->assertArrayHasKey('entity_method:config_test.query:concatProtectedProperty', $definitions);
}
/**
* @see \Drupal\Core\Config\Action\ConfigActionManager
*/
public function testMissingAction(): void {
$this->expectException(PluginNotFoundException::class);
$this->expectExceptionMessageMatches('/^The "does_not_exist" plugin does not exist/');
$this->container->get('plugin.manager.config_action')->applyAction('does_not_exist', 'config_test.system', ['foo' => 'Yay!']);
}
}

View File

@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\config_override_test\Cache\PirateDayCacheContext;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests if configuration overrides correctly affect cacheability metadata.
*
* @group config
*/
class CacheabilityMetadataConfigOverrideTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'block',
'block_content',
'config',
'config_override_test',
'field',
'path_alias',
'system',
'text',
'user',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->container->get('theme_installer')->install(['stark']);
$this->installEntitySchema('block_content');
$this->installConfig([
'block_content',
'config_override_test',
]);
}
/**
* Tests if config overrides correctly set cacheability metadata.
*/
public function testConfigOverride(): void {
// It's pirate day today!
$GLOBALS['it_is_pirate_day'] = TRUE;
$config_factory = $this->container->get('config.factory');
$config = $config_factory->get('system.theme');
// Check that we are using the Pirate theme.
$theme = $config->get('default');
$this->assertEquals('pirate', $theme);
// Check that the cacheability metadata is correct.
$this->assertEquals(['pirate_day'], $config->getCacheContexts());
$this->assertEquals(['config:system.theme', 'pirate-day-tag'], $config->getCacheTags());
$this->assertEquals(PirateDayCacheContext::PIRATE_DAY_MAX_AGE, $config->getCacheMaxAge());
}
/**
* Tests if config overrides set cacheability metadata on config entities.
*/
public function testConfigEntityOverride(): void {
// It's pirate day today!
$GLOBALS['it_is_pirate_day'] = TRUE;
// Load the User login block and check that its cacheability metadata is
// overridden correctly. This verifies that the metadata is correctly
// applied to config entities.
/** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
$entity_type_manager = $this->container->get('entity_type.manager');
$block = $entity_type_manager->getStorage('block')->load('call_to_action');
// Check that our call to action message is appealing to filibusters.
$this->assertEquals('Draw yer cutlasses!', $block->label());
// Check that the cacheability metadata is correct.
$this->assertEquals(['pirate_day'], $block->getCacheContexts());
$this->assertEquals(['config:block.block.call_to_action', 'pirate-day-tag'], $block->getCacheTags());
$this->assertEquals(PirateDayCacheContext::PIRATE_DAY_MAX_AGE, $block->getCacheMaxAge());
// Check that duplicating a config entity does not have the original config
// entity's cache tag.
$this->assertEquals(['config:block.block.', 'pirate-day-tag'], $block->createDuplicate()->getCacheTags());
// Check that renaming a config entity does not have the original config
// entity's cache tag.
$block->set('id', 'call_to_looting')->save();
$this->assertEquals(['pirate_day'], $block->getCacheContexts());
$this->assertEquals(['config:block.block.call_to_looting', 'pirate-day-tag'], $block->getCacheTags());
$this->assertEquals(PirateDayCacheContext::PIRATE_DAY_MAX_AGE, $block->getCacheMaxAge());
}
}

View File

@ -0,0 +1,352 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Config\Config;
use Drupal\Core\Config\ConfigNameException;
use Drupal\Core\Config\ConfigValueException;
use Drupal\Core\Config\DatabaseStorage;
use Drupal\Core\Config\UnsupportedDataTypeConfigException;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests CRUD operations on configuration objects.
*
* @group config
*/
class ConfigCRUDTest extends KernelTestBase {
/**
* Exempt from strict schema checking.
*
* @var bool
*
* @see \Drupal\Core\Config\Development\ConfigSchemaChecker
*/
protected $strictConfigSchema = FALSE;
/**
* {@inheritdoc}
*/
protected static $modules = ['system'];
/**
* Tests CRUD operations.
*/
public function testCRUD(): void {
$event_dispatcher = $this->container->get('event_dispatcher');
$typed_config_manager = $this->container->get('config.typed');
$storage = $this->container->get('config.storage');
$collection_storage = $storage->createCollection('test_collection');
$config_factory = $this->container->get('config.factory');
$name = 'config_test.crud';
// Create a new configuration object in the default collection.
$config = $this->config($name);
$this->assertTrue($config->isNew());
$config->set('value', 'initial');
$config->save();
$this->assertFalse($config->isNew());
// Verify the active configuration contains the saved value.
$actual_data = $storage->read($name);
$this->assertSame(['value' => 'initial'], $actual_data);
// Verify the config factory contains the saved value.
$actual_data = $config_factory->get($name)->getRawData();
$this->assertSame(['value' => 'initial'], $actual_data);
// Create another instance of the config object using a custom collection.
$collection_config = new Config(
$name,
$collection_storage,
$event_dispatcher,
$typed_config_manager
);
$collection_config->set('value', 'overridden');
$collection_config->save();
// Verify that the config factory still returns the right value, from the
// config instance in the default collection.
$actual_data = $config_factory->get($name)->getRawData();
$this->assertSame(['value' => 'initial'], $actual_data);
// Update the configuration object instance.
$config->set('value', 'instance-update');
$config->save();
$this->assertFalse($config->isNew());
// Verify the active configuration contains the updated value.
$actual_data = $storage->read($name);
$this->assertSame(['value' => 'instance-update'], $actual_data);
// Verify a call to $this->config() immediately returns the updated value.
$new_config = $this->config($name);
$this->assertSame($config->get(), $new_config->get());
$this->assertFalse($config->isNew());
// Pollute the config factory static cache.
$config_factory->getEditable($name);
// Delete the config object that uses a custom collection. This should not
// affect the instance returned by the config factory which depends on the
// default collection storage.
$collection_config->delete();
$actual_config = $config_factory->get($name);
$this->assertFalse($actual_config->isNew());
$this->assertSame(['value' => 'instance-update'], $actual_config->getRawData());
// Delete the configuration object.
$config->delete();
// Verify the configuration object is empty.
$this->assertSame([], $config->get());
$this->assertTrue($config->isNew());
// Verify that all copies of the configuration has been removed from the
// static cache.
$this->assertTrue($config_factory->getEditable($name)->isNew());
// Verify the active configuration contains no value.
$actual_data = $storage->read($name);
$this->assertFalse($actual_data);
// Verify $this->config() returns no data.
$new_config = $this->config($name);
$this->assertSame($config->get(), $new_config->get());
$this->assertTrue($config->isNew());
// Re-create the configuration object.
$config->set('value', 're-created');
$config->save();
$this->assertFalse($config->isNew());
// Verify the active configuration contains the updated value.
$actual_data = $storage->read($name);
$this->assertSame(['value' => 're-created'], $actual_data);
// Verify a call to $this->config() immediately returns the updated value.
$new_config = $this->config($name);
$this->assertSame($config->get(), $new_config->get());
$this->assertFalse($config->isNew());
// Rename the configuration object.
$new_name = 'config_test.crud_rename';
$this->container->get('config.factory')->rename($name, $new_name);
$renamed_config = $this->config($new_name);
$this->assertSame($config->get(), $renamed_config->get());
$this->assertFalse($renamed_config->isNew());
// Ensure that the old configuration object is removed from both the cache
// and the configuration storage.
$config = $this->config($name);
$this->assertSame([], $config->get());
$this->assertTrue($config->isNew());
// Test renaming when config.factory does not have the object in its static
// cache.
$name = 'config_test.crud_rename';
// Pollute the non-overrides static cache.
$config_factory->getEditable($name);
// Pollute the overrides static cache.
$config = $config_factory->get($name);
// Rename and ensure that happened properly.
$new_name = 'config_test.crud_rename_no_cache';
$config_factory->rename($name, $new_name);
$renamed_config = $config_factory->get($new_name);
$this->assertSame($config->get(), $renamed_config->get());
$this->assertFalse($renamed_config->isNew());
// Ensure the overrides static cache has been cleared.
$this->assertTrue($config_factory->get($name)->isNew());
// Ensure the non-overrides static cache has been cleared.
$this->assertTrue($config_factory->getEditable($name)->isNew());
// Merge data into the configuration object.
$new_config = $this->config($new_name);
$expected_values = [
'value' => 'herp',
'404' => 'foo',
];
$new_config->merge($expected_values);
$new_config->save();
$this->assertSame($expected_values['value'], $new_config->get('value'));
$this->assertSame($expected_values['404'], $new_config->get('404'));
// Test that getMultiple() does not return new config objects that were
// previously accessed with get()
$new_config = $config_factory->get('non_existing_key');
$this->assertTrue($new_config->isNew());
$this->assertCount(0, $config_factory->loadMultiple(['non_existing_key']), 'loadMultiple() does not return new objects');
}
/**
* Tests the validation of configuration object names.
*/
public function testNameValidation(): void {
// Verify that an object name without namespace causes an exception.
$name = 'no_namespace';
try {
$this->config($name)->save();
$this->fail('Expected ConfigNameException was thrown for a name without a namespace.');
}
catch (\Exception $e) {
$this->assertInstanceOf(ConfigNameException::class, $e);
}
// Verify that a name longer than the maximum length causes an exception.
$name = 'config_test.herman_melville.dick_or_the_whale.harper_1851.now_small_fowls_flew_screaming_over_the_yet_yawning_gulf_a_sullen_white_surf_beat_against_its_steep_sides_then_all_collapsed_and_the_great_shroud_of_the_sea_rolled_on_as_it_rolled_five_thousand_years_ago';
try {
$this->config($name)->save();
$this->fail('Expected ConfigNameException was thrown for a name longer than Config::MAX_NAME_LENGTH.');
}
catch (\Exception $e) {
$this->assertInstanceOf(ConfigNameException::class, $e);
}
// Verify that disallowed characters in the name cause an exception.
$characters = $test_characters = [':', '?', '*', '<', '>', '"', '\'', '/', '\\'];
foreach ($test_characters as $i => $c) {
try {
$name = 'namespace.object' . $c;
$config = $this->config($name);
$config->save();
}
catch (ConfigNameException) {
unset($test_characters[$i]);
}
}
$this->assertEmpty($test_characters, sprintf('Expected ConfigNameException was thrown for all invalid name characters: %s', implode(' ', $characters)));
// Verify that a valid config object name can be saved.
$name = 'namespace.object';
try {
$config = $this->config($name);
$config->save();
}
catch (ConfigNameException) {
$this->fail('ConfigNameException was not thrown for a valid object name.');
}
}
/**
* Tests the validation of configuration object values.
*/
public function testValueValidation(): void {
// Verify that setData() will catch dotted keys.
try {
$this->config('namespace.object')->setData(['key.value' => 12])->save();
$this->fail('Expected ConfigValueException was thrown from setData() for value with dotted keys.');
}
catch (\Exception $e) {
$this->assertInstanceOf(ConfigValueException::class, $e);
}
// Verify that set() will catch dotted keys.
try {
$this->config('namespace.object')->set('foo', ['key.value' => 12])->save();
$this->fail('Expected ConfigValueException was thrown from set() for value with dotted keys.');
}
catch (\Exception $e) {
$this->assertInstanceOf(ConfigValueException::class, $e);
}
}
/**
* Tests data type handling.
*/
public function testDataTypes(): void {
\Drupal::service('module_installer')->install(['config_test']);
$storage = new DatabaseStorage($this->container->get('database'), 'config');
$name = 'config_test.types';
$config = $this->config($name);
// Verify variable data types are intact.
$data = [
'array' => [],
'boolean' => TRUE,
'exp' => 1.2e+34,
'float' => 3.14159,
'float_as_integer' => (float) 1,
'hex' => 0xC,
'int' => 99,
// Symfony 5.1's YAML parser issues a deprecation when reading octal with
// a leading zero, to comply with YAML 1.2. However PECL YAML is still
// YAML 1.1 compliant.
// @todo Revisit parsing of octal once PECL YAML supports YAML 1.2.
// https://www.drupal.org/project/drupal/issues/3205480
// 'octal' => 0775,
'string' => 'string',
'string_int' => '1',
'nullable_array' => NULL,
'nullable_boolean' => NULL,
'nullable_exp' => NULL,
'nullable_float' => NULL,
'nullable_float_as_integer' => NULL,
'nullable_hex' => NULL,
'nullable_int' => NULL,
'nullable_octal' => NULL,
'nullable_string' => NULL,
'nullable_string_int' => NULL,
'mapping_with_only_required_keys' => [],
'mapping_with_some_required_keys' => [],
'mapping_with_only_optional_keys' => [],
];
$data = ['_core' => ['default_config_hash' => Crypt::hashBase64(serialize($data))]] + $data;
$this->assertSame($data, $config->get());
// Re-set each key using Config::set().
foreach ($data as $key => $value) {
$config->set($key, $value);
}
$config->save();
$this->assertSame($data, $config->get());
// Assert the data against the file storage.
$this->assertSame($data, $storage->read($name));
// Set data using config::setData().
$config->setData($data)->save();
$this->assertSame($data, $config->get());
$this->assertSame($data, $storage->read($name));
// Test that schema type enforcement can be overridden by trusting the data.
$this->assertSame(99, $config->get('int'));
$config->set('int', '99')->save(TRUE);
$this->assertSame('99', $config->get('int'));
// Test that re-saving without testing the data enforces the schema type.
$config->save();
$this->assertSame($data, $config->get());
// Test that setting an unsupported type for a config object with a schema
// fails.
try {
$config->set('stream', fopen(__FILE__, 'r'))->save();
$this->fail('No Exception thrown upon saving invalid data type.');
}
catch (UnsupportedDataTypeConfigException) {
// Expected exception; just continue testing.
}
// Test that setting an unsupported type for a config object with no schema
// also fails.
$typed_config_manager = $this->container->get('config.typed');
$config_name = 'config_test.no_schema';
$config = $this->config($config_name);
$this->assertFalse($typed_config_manager->hasConfigSchema($config_name));
try {
$config->set('stream', fopen(__FILE__, 'r'))->save();
$this->fail('No Exception thrown upon saving invalid data type.');
}
catch (UnsupportedDataTypeConfigException) {
// Expected exception; just continue testing.
}
}
}

View File

@ -0,0 +1,712 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\user\Entity\Role;
/**
* Tests for configuration dependencies.
*
* @coversDefaultClass \Drupal\Core\Config\ConfigManager
*
* @group config
*/
class ConfigDependencyTest extends EntityKernelTestBase {
/**
* Modules to install.
*
* The entity_test module is enabled to provide content entity types.
*
* @var array
*/
protected static $modules = ['config_test', 'entity_test', 'user', 'node', 'views'];
/**
* Tests that calculating dependencies for system module.
*/
public function testNonEntity(): void {
$this->installConfig(['system']);
$config_manager = \Drupal::service('config.manager');
$dependents = $config_manager->findConfigEntityDependencies('module', ['system']);
$this->assertTrue(isset($dependents['system.site']), 'Simple configuration system.site has a UUID key even though it is not a configuration entity and therefore is found when looking for dependencies of the System module.');
// Ensure that calling
// \Drupal\Core\Config\ConfigManager::findConfigEntityDependenciesAsEntities()
// does not try to load system.site as an entity.
$config_manager->findConfigEntityDependenciesAsEntities('module', ['system']);
}
/**
* Tests creating dependencies on configuration entities.
*/
public function testDependencyManagement(): void {
/** @var \Drupal\Core\Config\ConfigManagerInterface $config_manager */
$config_manager = \Drupal::service('config.manager');
$storage = $this->container->get('entity_type.manager')->getStorage('config_test');
// Test dependencies between modules.
$entity1 = $storage->create(
[
'id' => 'entity1',
'dependencies' => [
'enforced' => [
'module' => ['node'],
],
],
]
);
$entity1->save();
$dependents = $config_manager->findConfigEntityDependencies('module', ['node']);
$this->assertTrue(isset($dependents['config_test.dynamic.entity1']), 'config_test.dynamic.entity1 has a dependency on the Node module.');
$dependents = $config_manager->findConfigEntityDependencies('module', ['config_test']);
$this->assertTrue(isset($dependents['config_test.dynamic.entity1']), 'config_test.dynamic.entity1 has a dependency on the config_test module.');
$dependents = $config_manager->findConfigEntityDependencies('module', ['views']);
$this->assertFalse(isset($dependents['config_test.dynamic.entity1']), 'config_test.dynamic.entity1 does not have a dependency on the Views module.');
// Ensure that the provider of the config entity is not actually written to
// the dependencies array.
$raw_config = $this->config('config_test.dynamic.entity1');
$root_module_dependencies = $raw_config->get('dependencies.module');
$this->assertEmpty($root_module_dependencies, 'Node module is not written to the root dependencies array as it is enforced.');
// Create additional entities to test dependencies on config entities.
$entity2 = $storage->create(['id' => 'entity2', 'dependencies' => ['enforced' => ['config' => [$entity1->getConfigDependencyName()]]]]);
$entity2->save();
$entity3 = $storage->create(['id' => 'entity3', 'dependencies' => ['enforced' => ['config' => [$entity2->getConfigDependencyName()]]]]);
$entity3->save();
$entity4 = $storage->create(['id' => 'entity4', 'dependencies' => ['enforced' => ['config' => [$entity3->getConfigDependencyName()]]]]);
$entity4->save();
// Test getting $entity1's dependencies as configuration dependency objects.
$dependents = $config_manager->findConfigEntityDependencies('config', [$entity1->getConfigDependencyName()]);
$this->assertFalse(isset($dependents['config_test.dynamic.entity1']), 'config_test.dynamic.entity1 does not have a dependency on itself.');
$this->assertTrue(isset($dependents['config_test.dynamic.entity2']), 'config_test.dynamic.entity2 has a dependency on config_test.dynamic.entity1.');
$this->assertTrue(isset($dependents['config_test.dynamic.entity3']), 'config_test.dynamic.entity3 has a dependency on config_test.dynamic.entity1.');
$this->assertTrue(isset($dependents['config_test.dynamic.entity4']), 'config_test.dynamic.entity4 has a dependency on config_test.dynamic.entity1.');
// Test getting $entity2's dependencies as entities.
$dependents = $config_manager->findConfigEntityDependenciesAsEntities('config', [$entity2->getConfigDependencyName()]);
$dependent_ids = $this->getDependentIds($dependents);
$this->assertNotContains('config_test:entity1', $dependent_ids, 'config_test.dynamic.entity1 does not have a dependency on config_test.dynamic.entity1.');
$this->assertNotContains('config_test:entity2', $dependent_ids, 'config_test.dynamic.entity2 does not have a dependency on itself.');
$this->assertContains('config_test:entity3', $dependent_ids, 'config_test.dynamic.entity3 has a dependency on config_test.dynamic.entity2.');
$this->assertContains('config_test:entity4', $dependent_ids, 'config_test.dynamic.entity4 has a dependency on config_test.dynamic.entity2.');
// Test getting node module's dependencies as configuration dependency
// objects.
$dependents = $config_manager->findConfigEntityDependencies('module', ['node']);
$this->assertTrue(isset($dependents['config_test.dynamic.entity1']), 'config_test.dynamic.entity1 has a dependency on the Node module.');
$this->assertTrue(isset($dependents['config_test.dynamic.entity2']), 'config_test.dynamic.entity2 has a dependency on the Node module.');
$this->assertTrue(isset($dependents['config_test.dynamic.entity3']), 'config_test.dynamic.entity3 has a dependency on the Node module.');
$this->assertTrue(isset($dependents['config_test.dynamic.entity4']), 'config_test.dynamic.entity4 has a dependency on the Node module.');
// Test getting node module's dependencies as configuration dependency
// objects after making $entity3 also dependent on node module but $entity1
// no longer depend on node module.
$entity1->setEnforcedDependencies([])->save();
$entity3->setEnforcedDependencies(['module' => ['node'], 'config' => [$entity2->getConfigDependencyName()]])->save();
$dependents = $config_manager->findConfigEntityDependencies('module', ['node']);
$this->assertFalse(isset($dependents['config_test.dynamic.entity1']), 'config_test.dynamic.entity1 does not have a dependency on the Node module.');
$this->assertFalse(isset($dependents['config_test.dynamic.entity2']), 'config_test.dynamic.entity2 does not have a dependency on the Node module.');
$this->assertTrue(isset($dependents['config_test.dynamic.entity3']), 'config_test.dynamic.entity3 has a dependency on the Node module.');
$this->assertTrue(isset($dependents['config_test.dynamic.entity4']), 'config_test.dynamic.entity4 has a dependency on the Node module.');
// Test dependency on a content entity.
$entity_test = EntityTest::create([
'name' => $this->randomString(),
'type' => 'entity_test',
]);
$entity_test->save();
$entity2->setEnforcedDependencies(['config' => [$entity1->getConfigDependencyName()], 'content' => [$entity_test->getConfigDependencyName()]])->save();
$dependents = $config_manager->findConfigEntityDependencies('content', [$entity_test->getConfigDependencyName()]);
$this->assertFalse(isset($dependents['config_test.dynamic.entity1']), 'config_test.dynamic.entity1 does not have a dependency on the content entity.');
$this->assertTrue(isset($dependents['config_test.dynamic.entity2']), 'config_test.dynamic.entity2 has a dependency on the content entity.');
$this->assertTrue(isset($dependents['config_test.dynamic.entity3']), 'config_test.dynamic.entity3 has a dependency on the content entity (via entity2).');
$this->assertTrue(isset($dependents['config_test.dynamic.entity4']), 'config_test.dynamic.entity4 has a dependency on the content entity (via entity3).');
// Create a configuration entity of a different type with the same ID as one
// of the entities already created.
$alt_storage = $this->container->get('entity_type.manager')->getStorage('config_query_test');
$alt_storage->create(['id' => 'entity1', 'dependencies' => ['enforced' => ['config' => [$entity1->getConfigDependencyName()]]]])->save();
$alt_storage->create(['id' => 'entity2', 'dependencies' => ['enforced' => ['module' => ['views']]]])->save();
$dependents = $config_manager->findConfigEntityDependenciesAsEntities('config', [$entity1->getConfigDependencyName()]);
$dependent_ids = $this->getDependentIds($dependents);
$this->assertNotContains('config_test:entity1', $dependent_ids, 'config_test.dynamic.entity1 does not have a dependency on itself.');
$this->assertContains('config_test:entity2', $dependent_ids, 'config_test.dynamic.entity2 has a dependency on config_test.dynamic.entity1.');
$this->assertContains('config_test:entity3', $dependent_ids, 'config_test.dynamic.entity3 has a dependency on config_test.dynamic.entity1.');
$this->assertContains('config_test:entity4', $dependent_ids, 'config_test.dynamic.entity4 has a dependency on config_test.dynamic.entity1.');
$this->assertContains('config_query_test:entity1', $dependent_ids, 'config_query_test.dynamic.entity1 has a dependency on config_test.dynamic.entity1.');
$this->assertNotContains('config_query_test:entity2', $dependent_ids, 'config_query_test.dynamic.entity2 does not have a dependency on config_test.dynamic.entity1.');
$dependents = $config_manager->findConfigEntityDependenciesAsEntities('module', ['node', 'views']);
$dependent_ids = $this->getDependentIds($dependents);
$this->assertNotContains('config_test:entity1', $dependent_ids, 'config_test.dynamic.entity1 does not have a dependency on Views or Node.');
$this->assertNotContains('config_test:entity2', $dependent_ids, 'config_test.dynamic.entity2 does not have a dependency on Views or Node.');
$this->assertContains('config_test:entity3', $dependent_ids, 'config_test.dynamic.entity3 has a dependency on Views or Node.');
$this->assertContains('config_test:entity4', $dependent_ids, 'config_test.dynamic.entity4 has a dependency on Views or Node.');
$this->assertNotContains('config_query_test:entity1', $dependent_ids, 'config_test.query.entity1 does not have a dependency on Views or Node.');
$this->assertContains('config_query_test:entity2', $dependent_ids, 'config_test.query.entity2 has a dependency on Views or Node.');
$dependents = $config_manager->findConfigEntityDependenciesAsEntities('module', ['config_test']);
$dependent_ids = $this->getDependentIds($dependents);
$this->assertContains('config_test:entity1', $dependent_ids, 'config_test.dynamic.entity1 has a dependency on config_test module.');
$this->assertContains('config_test:entity2', $dependent_ids, 'config_test.dynamic.entity2 has a dependency on config_test module.');
$this->assertContains('config_test:entity3', $dependent_ids, 'config_test.dynamic.entity3 has a dependency on config_test module.');
$this->assertContains('config_test:entity4', $dependent_ids, 'config_test.dynamic.entity4 has a dependency on config_test module.');
$this->assertContains('config_query_test:entity1', $dependent_ids, 'config_test.query.entity1 has a dependency on config_test module.');
$this->assertContains('config_query_test:entity2', $dependent_ids, 'config_test.query.entity2 has a dependency on config_test module.');
// Test the ability to find missing content dependencies.
$missing_dependencies = $config_manager->findMissingContentDependencies();
$this->assertEquals([], $missing_dependencies);
$expected = [
$entity_test->uuid() => [
'entity_type' => 'entity_test',
'bundle' => $entity_test->bundle(),
'uuid' => $entity_test->uuid(),
],
];
// Delete the content entity so that is it now missing.
$entity_test->delete();
$missing_dependencies = $config_manager->findMissingContentDependencies();
$this->assertEquals($expected, $missing_dependencies);
// Add a fake missing dependency to ensure multiple missing dependencies
// work.
$entity1->setEnforcedDependencies(['content' => [$entity_test->getConfigDependencyName(), 'entity_test:bundle:uuid']])->save();
$expected['uuid'] = [
'entity_type' => 'entity_test',
'bundle' => 'bundle',
'uuid' => 'uuid',
];
$missing_dependencies = $config_manager->findMissingContentDependencies();
$this->assertEquals($expected, $missing_dependencies);
}
/**
* Tests ConfigManager::uninstall() and config entity dependency management.
*/
public function testConfigEntityUninstall(): void {
/** @var \Drupal\Core\Config\ConfigManagerInterface $config_manager */
$config_manager = \Drupal::service('config.manager');
/** @var \Drupal\Core\Config\Entity\ConfigEntityStorage $storage */
$storage = $this->container->get('entity_type.manager')
->getStorage('config_test');
// Test dependencies between modules.
$entity1 = $storage->create(
[
'id' => 'entity1',
'dependencies' => [
'enforced' => [
'module' => ['node', 'config_test'],
],
],
]
);
$entity1->save();
$entity2 = $storage->create(
[
'id' => 'entity2',
'dependencies' => [
'enforced' => [
'config' => [$entity1->getConfigDependencyName()],
],
],
]
);
$entity2->save();
// Test that doing a config uninstall of the node module deletes entity2
// since it is dependent on entity1 which is dependent on the node module.
$config_manager->uninstall('module', 'node');
$this->assertNull($storage->load('entity1'), 'Entity 1 deleted');
$this->assertNull($storage->load('entity2'), 'Entity 2 deleted');
}
/**
* Data provider for self::testConfigEntityUninstallComplex().
*/
public static function providerConfigEntityUninstallComplex() {
// Ensure that alphabetical order has no influence on dependency fixing and
// removal.
return [
[['a', 'b', 'c', 'd', 'e']],
[['e', 'd', 'c', 'b', 'a']],
[['e', 'c', 'd', 'a', 'b']],
];
}
/**
* Tests complex configuration entity dependency handling during uninstall.
*
* Configuration entities can be deleted or updated during module uninstall
* because they have dependencies on the module.
*
* @param array $entity_id_suffixes
* The suffixes to add to the 4 entities created by the test.
*
* @dataProvider providerConfigEntityUninstallComplex
*/
public function testConfigEntityUninstallComplex(array $entity_id_suffixes): void {
/** @var \Drupal\Core\Config\ConfigManagerInterface $config_manager */
$config_manager = \Drupal::service('config.manager');
/** @var \Drupal\Core\Config\Entity\ConfigEntityStorage $storage */
$storage = $this->container->get('entity_type.manager')
->getStorage('config_test');
// Entity 1 will be deleted because it depends on node.
$entity_1 = $storage->create(
[
'id' => 'entity_' . $entity_id_suffixes[0],
'dependencies' => [
'enforced' => [
'module' => ['node', 'config_test'],
],
],
]
);
$entity_1->save();
// Entity 2 has a dependency on entity 1 but it can be fixed because
// \Drupal\config_test\Entity::onDependencyRemoval() will remove the
// dependency before config entities are deleted.
$entity_2 = $storage->create(
[
'id' => 'entity_' . $entity_id_suffixes[1],
'dependencies' => [
'enforced' => [
'config' => [$entity_1->getConfigDependencyName()],
],
],
]
);
$entity_2->save();
// Entity 3 will be unchanged because it is dependent on entity 2 which can
// be fixed. The ConfigEntityInterface::onDependencyRemoval() method will
// not be called for this entity.
$entity_3 = $storage->create(
[
'id' => 'entity_' . $entity_id_suffixes[2],
'dependencies' => [
'enforced' => [
'config' => [$entity_2->getConfigDependencyName()],
],
],
]
);
$entity_3->save();
// Entity 4's config dependency will be fixed but it will still be deleted
// because it also depends on the node module.
$entity_4 = $storage->create(
[
'id' => 'entity_' . $entity_id_suffixes[3],
'dependencies' => [
'enforced' => [
'config' => [$entity_1->getConfigDependencyName()],
'module' => ['node', 'config_test'],
],
],
]
);
$entity_4->save();
// Entity 5 will be fixed because it is dependent on entity 3, which is
// unchanged, and entity 1 which will be fixed because
// \Drupal\config_test\Entity::onDependencyRemoval() will remove the
// dependency.
$entity_5 = $storage->create(
[
'id' => 'entity_' . $entity_id_suffixes[4],
'dependencies' => [
'enforced' => [
'config' => [
$entity_1->getConfigDependencyName(),
$entity_3->getConfigDependencyName(),
],
],
],
]
);
$entity_5->save();
// Set a more complicated test where dependencies will be fixed.
\Drupal::state()->set('config_test.fix_dependencies', [$entity_1->getConfigDependencyName()]);
\Drupal::state()->set('config_test.on_dependency_removal_called', []);
// Do a dry run using
// \Drupal\Core\Config\ConfigManager::getConfigEntitiesToChangeOnDependencyRemoval().
$config_entities = $config_manager->getConfigEntitiesToChangeOnDependencyRemoval('module', ['node']);
// Assert that \Drupal\config_test\Entity\ConfigTest::onDependencyRemoval()
// is called as expected and with the correct dependencies.
$called = \Drupal::state()->get('config_test.on_dependency_removal_called', []);
$this->assertArrayNotHasKey($entity_3->id(), $called, 'ConfigEntityInterface::onDependencyRemoval() is not called for entity 3.');
$this->assertSame([$entity_1->id(), $entity_4->id(), $entity_2->id(), $entity_5->id()], array_keys($called), 'The most dependent entities have ConfigEntityInterface::onDependencyRemoval() called first.');
$this->assertSame(['config' => [], 'content' => [], 'module' => ['node'], 'theme' => []], $called[$entity_1->id()]);
$this->assertSame(['config' => [$entity_1->getConfigDependencyName()], 'content' => [], 'module' => [], 'theme' => []], $called[$entity_2->id()]);
$this->assertSame(['config' => [$entity_1->getConfigDependencyName()], 'content' => [], 'module' => ['node'], 'theme' => []], $called[$entity_4->id()]);
$this->assertSame(['config' => [$entity_1->getConfigDependencyName()], 'content' => [], 'module' => [], 'theme' => []], $called[$entity_5->id()]);
$this->assertEquals($entity_1->uuid(), $config_entities['delete'][1]->uuid(), 'Entity 1 will be deleted.');
$this->assertEquals($entity_2->uuid(), $config_entities['update'][0]->uuid(), 'Entity 2 will be updated.');
$this->assertEquals($entity_3->uuid(), reset($config_entities['unchanged'])->uuid(), 'Entity 3 is not changed.');
$this->assertEquals($entity_4->uuid(), $config_entities['delete'][0]->uuid(), 'Entity 4 will be deleted.');
$this->assertEquals($entity_5->uuid(), $config_entities['update'][1]->uuid(), 'Entity 5 is updated.');
// Perform the uninstall.
$config_manager->uninstall('module', 'node');
// Test that expected actions have been performed.
$this->assertNull($storage->load($entity_1->id()), 'Entity 1 deleted');
$entity_2 = $storage->load($entity_2->id());
$this->assertNotEmpty($entity_2, 'Entity 2 not deleted');
$this->assertEquals([], $entity_2->calculateDependencies()->getDependencies()['config'], 'Entity 2 dependencies updated to remove dependency on entity 1.');
$entity_3 = $storage->load($entity_3->id());
$this->assertNotEmpty($entity_3, 'Entity 3 not deleted');
$this->assertEquals([$entity_2->getConfigDependencyName()], $entity_3->calculateDependencies()->getDependencies()['config'], 'Entity 3 still depends on entity 2.');
$this->assertNull($storage->load($entity_4->id()), 'Entity 4 deleted');
}
/**
* @covers ::uninstall
* @covers ::getConfigEntitiesToChangeOnDependencyRemoval
*/
public function testConfigEntityUninstallThirdParty(): void {
/** @var \Drupal\Core\Config\ConfigManagerInterface $config_manager */
$config_manager = \Drupal::service('config.manager');
/** @var \Drupal\Core\Config\Entity\ConfigEntityStorage $storage */
$storage = $this->container->get('entity_type.manager')
->getStorage('config_test');
// Entity 1 will be fixed because it only has a dependency via third-party
// settings, which are fixable.
$entity_1 = $storage->create([
'id' => 'entity_1',
'dependencies' => [
'enforced' => [
'module' => ['config_test'],
],
],
'third_party_settings' => [
'node' => [
'foo' => 'bar',
],
],
]);
$entity_1->save();
// Entity 2 has a dependency on entity 1.
$entity_2 = $storage->create([
'id' => 'entity_2',
'dependencies' => [
'enforced' => [
'config' => [$entity_1->getConfigDependencyName()],
],
],
'third_party_settings' => [
'node' => [
'foo' => 'bar',
],
],
]);
$entity_2->save();
// Entity 3 will be unchanged because it is dependent on entity 2 which can
// be fixed. The ConfigEntityInterface::onDependencyRemoval() method will
// not be called for this entity.
$entity_3 = $storage->create([
'id' => 'entity_3',
'dependencies' => [
'enforced' => [
'config' => [$entity_2->getConfigDependencyName()],
],
],
]);
$entity_3->save();
// Entity 4's config dependency will be fixed but it will still be deleted
// because it also depends on the node module.
$entity_4 = $storage->create([
'id' => 'entity_4',
'dependencies' => [
'enforced' => [
'config' => [$entity_1->getConfigDependencyName()],
'module' => ['node', 'config_test'],
],
],
]);
$entity_4->save();
\Drupal::state()->set('config_test.fix_dependencies', []);
\Drupal::state()->set('config_test.on_dependency_removal_called', []);
// Do a dry run using
// \Drupal\Core\Config\ConfigManager::getConfigEntitiesToChangeOnDependencyRemoval().
$config_entities = $config_manager->getConfigEntitiesToChangeOnDependencyRemoval('module', ['node']);
$config_entity_ids = [
'update' => [],
'delete' => [],
'unchanged' => [],
];
foreach ($config_entities as $type => $config_entities_by_type) {
foreach ($config_entities_by_type as $config_entity) {
$config_entity_ids[$type][] = $config_entity->id();
}
}
$expected = [
'update' => [$entity_1->id(), $entity_2->id()],
'delete' => [$entity_4->id()],
'unchanged' => [$entity_3->id()],
];
$this->assertSame($expected, $config_entity_ids);
$called = \Drupal::state()->get('config_test.on_dependency_removal_called', []);
$this->assertArrayNotHasKey($entity_3->id(), $called, 'ConfigEntityInterface::onDependencyRemoval() is not called for entity 3.');
$this->assertSame([$entity_1->id(), $entity_4->id(), $entity_2->id()], array_keys($called), 'The most dependent entities have ConfigEntityInterface::onDependencyRemoval() called first.');
$this->assertSame(['config' => [], 'content' => [], 'module' => ['node'], 'theme' => []], $called[$entity_1->id()]);
$this->assertSame(['config' => [], 'content' => [], 'module' => ['node'], 'theme' => []], $called[$entity_2->id()]);
$this->assertSame(['config' => [], 'content' => [], 'module' => ['node'], 'theme' => []], $called[$entity_4->id()]);
// Perform the uninstall.
$config_manager->uninstall('module', 'node');
// Test that expected actions have been performed.
$entity_1 = $storage->load($entity_1->id());
$this->assertNotEmpty($entity_1, 'Entity 1 not deleted');
$this->assertSame($entity_1->getThirdPartySettings('node'), [], 'Entity 1 third party settings updated.');
$entity_2 = $storage->load($entity_2->id());
$this->assertNotEmpty($entity_2, 'Entity 2 not deleted');
$this->assertSame($entity_2->getThirdPartySettings('node'), [], 'Entity 2 third party settings updated.');
$this->assertSame($entity_2->calculateDependencies()->getDependencies()['config'], [$entity_1->getConfigDependencyName()], 'Entity 2 still depends on entity 1.');
$entity_3 = $storage->load($entity_3->id());
$this->assertNotEmpty($entity_3, 'Entity 3 not deleted');
$this->assertSame($entity_3->calculateDependencies()->getDependencies()['config'], [$entity_2->getConfigDependencyName()], 'Entity 3 still depends on entity 2.');
$this->assertNull($storage->load($entity_4->id()), 'Entity 4 deleted');
}
/**
* Tests deleting a configuration entity and dependency management.
*/
public function testConfigEntityDelete(): void {
/** @var \Drupal\Core\Config\ConfigManagerInterface $config_manager */
$config_manager = \Drupal::service('config.manager');
/** @var \Drupal\Core\Config\Entity\ConfigEntityStorage $storage */
$storage = $this->container->get('entity_type.manager')->getStorage('config_test');
// Test dependencies between configuration entities.
$entity1 = $storage->create(
[
'id' => 'entity1',
]
);
$entity1->save();
$entity2 = $storage->create(
[
'id' => 'entity2',
'dependencies' => [
'enforced' => [
'config' => [$entity1->getConfigDependencyName()],
],
],
]
);
$entity2->save();
// Do a dry run using
// \Drupal\Core\Config\ConfigManager::getConfigEntitiesToChangeOnDependencyRemoval().
$config_entities = $config_manager->getConfigEntitiesToChangeOnDependencyRemoval('config', [$entity1->getConfigDependencyName()]);
$this->assertEquals($entity2->uuid(), reset($config_entities['delete'])->uuid(), 'Entity 2 will be deleted.');
$this->assertEmpty($config_entities['update'], 'No dependent configuration entities will be updated.');
$this->assertEmpty($config_entities['unchanged'], 'No dependent configuration entities will be unchanged.');
// Test that doing a delete of entity1 deletes entity2 since it is dependent
// on entity1.
$entity1->delete();
$this->assertNull($storage->load('entity1'), 'Entity 1 deleted');
$this->assertNull($storage->load('entity2'), 'Entity 2 deleted');
// Set a more complicated test where dependencies will be fixed.
\Drupal::state()->set('config_test.fix_dependencies', [$entity1->getConfigDependencyName()]);
// Entity1 will be deleted by the test.
$entity1 = $storage->create(
[
'id' => 'entity1',
]
);
$entity1->save();
// Entity2 has a dependency on Entity1 but it can be fixed because
// \Drupal\config_test\Entity::onDependencyRemoval() will remove the
// dependency before config entities are deleted.
$entity2 = $storage->create(
[
'id' => 'entity2',
'dependencies' => [
'enforced' => [
'config' => [$entity1->getConfigDependencyName()],
],
],
]
);
$entity2->save();
// Entity3 will be unchanged because it is dependent on Entity2 which can
// be fixed.
$entity3 = $storage->create(
[
'id' => 'entity3',
'dependencies' => [
'enforced' => [
'config' => [$entity2->getConfigDependencyName()],
],
],
]
);
$entity3->save();
// Do a dry run using
// \Drupal\Core\Config\ConfigManager::getConfigEntitiesToChangeOnDependencyRemoval().
$config_entities = $config_manager->getConfigEntitiesToChangeOnDependencyRemoval('config', [$entity1->getConfigDependencyName()]);
$this->assertEmpty($config_entities['delete'], 'No dependent configuration entities will be deleted.');
$this->assertEquals($entity2->uuid(), reset($config_entities['update'])->uuid(), 'Entity 2 will be updated.');
$this->assertEquals($entity3->uuid(), reset($config_entities['unchanged'])->uuid(), 'Entity 3 is not changed.');
// Perform the uninstall.
$entity1->delete();
// Test that expected actions have been performed.
$this->assertNull($storage->load('entity1'), 'Entity 1 deleted');
$entity2 = $storage->load('entity2');
$this->assertNotEmpty($entity2, 'Entity 2 not deleted');
$this->assertEquals([], $entity2->calculateDependencies()->getDependencies()['config'], 'Entity 2 dependencies updated to remove dependency on Entity1.');
$entity3 = $storage->load('entity3');
$this->assertNotEmpty($entity3, 'Entity 3 not deleted');
$this->assertEquals($entity3->calculateDependencies()->getDependencies()['config'], [$entity2->getConfigDependencyName()], 'Entity 3 still depends on Entity 2.');
}
/**
* Tests getConfigEntitiesToChangeOnDependencyRemoval() with content entities.
*
* At the moment there is no runtime code that calculates configuration
* dependencies on content entity delete because this calculation is expensive
* and all content dependencies are soft. This test ensures that the code
* works for content entities.
*
* @see \Drupal\Core\Config\ConfigManager::getConfigEntitiesToChangeOnDependencyRemoval()
*/
public function testContentEntityDelete(): void {
$this->installEntitySchema('entity_test');
/** @var \Drupal\Core\Config\ConfigManagerInterface $config_manager */
$config_manager = \Drupal::service('config.manager');
$content_entity = EntityTest::create();
$content_entity->save();
/** @var \Drupal\Core\Config\Entity\ConfigEntityStorage $storage */
$storage = $this->container->get('entity_type.manager')->getStorage('config_test');
$entity1 = $storage->create(
[
'id' => 'entity1',
'dependencies' => [
'enforced' => [
'content' => [$content_entity->getConfigDependencyName()],
],
],
]
);
$entity1->save();
$entity2 = $storage->create(
[
'id' => 'entity2',
'dependencies' => [
'enforced' => [
'config' => [$entity1->getConfigDependencyName()],
],
],
]
);
$entity2->save();
// Create a configuration entity that is not in the dependency chain.
$entity3 = $storage->create(['id' => 'entity3']);
$entity3->save();
$config_entities = $config_manager->getConfigEntitiesToChangeOnDependencyRemoval('content', [$content_entity->getConfigDependencyName()]);
$this->assertEquals($entity1->uuid(), $config_entities['delete'][1]->uuid(), 'Entity 1 will be deleted.');
$this->assertEquals($entity2->uuid(), $config_entities['delete'][0]->uuid(), 'Entity 2 will be deleted.');
$this->assertEmpty($config_entities['update'], 'No dependencies of the content entity will be updated.');
$this->assertEmpty($config_entities['unchanged'], 'No dependencies of the content entity will be unchanged.');
}
/**
* Tests that config dependency ordering.
*/
public function testDependencyOrder(): void {
$storage = $this->container->get('entity_type.manager')->getStorage('config_test');
// Test dependencies between modules.
$entity1 = $storage->create(['id' => 'entity1']);
$entity1->save();
// Create additional entities to test dependencies on config entities.
$entity2 = $storage->create(['id' => 'entity2', 'dependencies' => ['enforced' => ['config' => [$entity1->getConfigDependencyName()]]]]);
$entity2->save();
$entity3 = $storage->create(['id' => 'entity3', 'dependencies' => ['enforced' => ['config' => [$entity1->getConfigDependencyName()]]]]);
$entity3->save();
// Include a role entity to test ordering when dependencies have multiple
// entity types.
$role = Role::create([
'id' => 'test_role',
'label' => 'Test role',
// This adds an implicit dependency on $entity 2, and hence also $entity1,
// to the role.
'permissions' => ["permission with {$entity2->getConfigDependencyName()} dependency"],
]);
$role->save();
$entity4 = $storage->create([
'id' => 'entity4',
'dependencies' => [
'enforced' => [
'config' => [
// Add dependencies to $entity3 and the role so that the $entity4
// should be last to be processed when handling dependency removal.
// The role should be processed after $entity1 and $entity2, but
// before $entity4.
$entity3->getConfigDependencyName(),
$role->getConfigDependencyName(),
],
],
],
]);
$entity4->save();
// Create scenario where entity1 is deleted, but all the config_test
// entities depending on entity1 are fixed instead of being deleted. This
// means that entity2 is not deleted, so the role should not lose the
// permission depending on entity2.
\Drupal::state()->set('config_test.fix_dependencies', ['config_test.dynamic.entity1']);
$entity1->delete();
$role = Role::load('test_role');
$this->assertNotNull($role);
$this->assertTrue($role->hasPermission("permission with {$entity2->getConfigDependencyName()} dependency"));
}
/**
* Gets a list of identifiers from an array of configuration entities.
*
* @param \Drupal\Core\Config\Entity\ConfigEntityInterface[] $dependents
* An array of configuration entities.
*
* @return array
* An array with values of entity_type_id:ID
*/
protected function getDependentIds(array $dependents): array {
$dependent_ids = [];
foreach ($dependents as $dependent) {
$dependent_ids[] = $dependent->getEntityTypeId() . ':' . $dependent->id();
}
return $dependent_ids;
}
}

View File

@ -0,0 +1,190 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\KernelTests\KernelTestBase;
/**
* Calculating the difference between two sets of configuration.
*
* @group config
*/
class ConfigDiffTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['config_test', 'system'];
/**
* Tests calculating the difference between two sets of configuration.
*/
public function testDiff(): void {
$active = $this->container->get('config.storage');
$sync = $this->container->get('config.storage.sync');
$config_name = 'config_test.system';
$change_key = 'foo';
$remove_key = '404';
$add_key = 'biff';
$add_data = 'bangPow';
$change_data = 'foobar';
// Install the default config.
$this->installConfig(['config_test']);
$original_data = \Drupal::config($config_name)->get();
// Change a configuration value in sync.
$sync_data = $original_data;
$sync_data[$change_key] = $change_data;
$sync_data[$add_key] = $add_data;
$sync->write($config_name, $sync_data);
// Verify that the diff reflects a change.
$diff = \Drupal::service('config.manager')->diff($active, $sync, $config_name);
$edits = $diff->getEdits();
$this->assertYamlEdit($edits, $change_key, 'change',
[$change_key . ': ' . $original_data[$change_key]],
[$change_key . ': ' . $change_data]);
// Reset data back to original, and remove a key
$sync_data = $original_data;
unset($sync_data[$remove_key]);
$sync->write($config_name, $sync_data);
// Verify that the diff reflects a removed key.
$diff = \Drupal::service('config.manager')->diff($active, $sync, $config_name);
$edits = $diff->getEdits();
$this->assertYamlEdit($edits, $change_key, 'copy');
$this->assertYamlEdit($edits, $remove_key, 'delete',
[$remove_key . ': ' . $original_data[$remove_key]],
FALSE
);
// Reset data back to original and add a key
$sync_data = $original_data;
$sync_data[$add_key] = $add_data;
$sync->write($config_name, $sync_data);
// Verify that the diff reflects an added key.
$diff = \Drupal::service('config.manager')->diff($active, $sync, $config_name);
$edits = $diff->getEdits();
$this->assertYamlEdit($edits, $change_key, 'copy');
$this->assertYamlEdit($edits, $add_key, 'add', FALSE, [$add_key . ': ' . $add_data]);
// Test diffing a renamed config entity.
$test_entity_id = $this->randomMachineName();
$test_entity = \Drupal::entityTypeManager()->getStorage('config_test')->create([
'id' => $test_entity_id,
'label' => $this->randomMachineName(),
]);
$test_entity->save();
$data = $active->read('config_test.dynamic.' . $test_entity_id);
$sync->write('config_test.dynamic.' . $test_entity_id, $data);
$config_name = 'config_test.dynamic.' . $test_entity_id;
$diff = \Drupal::service('config.manager')->diff($active, $sync, $config_name, $config_name);
// Prove the fields match.
$edits = $diff->getEdits();
$this->assertEquals('copy', $edits[0]->type, 'The first item in the diff is a copy.');
$this->assertCount(1, $edits, 'There is one item in the diff');
// Rename the entity.
$new_test_entity_id = $this->randomMachineName();
$test_entity->set('id', $new_test_entity_id);
$test_entity->save();
$diff = \Drupal::service('config.manager')->diff($active, $sync, 'config_test.dynamic.' . $new_test_entity_id, $config_name);
$edits = $diff->getEdits();
$this->assertYamlEdit($edits, 'uuid', 'copy');
$this->assertYamlEdit($edits, 'id', 'change',
['id: ' . $new_test_entity_id],
['id: ' . $test_entity_id]);
$this->assertYamlEdit($edits, 'label', 'copy');
$this->assertEquals('copy', $edits[2]->type, 'The third item in the diff is a copy.');
$this->assertCount(3, $edits, 'There are three items in the diff.');
}
/**
* Tests calculating the difference between two sets of config collections.
*/
public function testCollectionDiff(): void {
/** @var \Drupal\Core\Config\StorageInterface $active */
$active = $this->container->get('config.storage');
/** @var \Drupal\Core\Config\StorageInterface $sync */
$sync = $this->container->get('config.storage.sync');
$active_test_collection = $active->createCollection('test');
$sync_test_collection = $sync->createCollection('test');
$config_name = 'config_test.test';
$data = ['foo' => 'bar'];
$active->write($config_name, $data);
$sync->write($config_name, $data);
$active_test_collection->write($config_name, $data);
$sync_test_collection->write($config_name, ['foo' => 'baz']);
// Test the fields match in the default collection diff.
$diff = \Drupal::service('config.manager')->diff($active, $sync, $config_name);
$edits = $diff->getEdits();
$this->assertEquals('copy', $edits[0]->type, 'The first item in the diff is a copy.');
$this->assertCount(1, $edits, 'There is one item in the diff');
// Test that the differences are detected when diffing the collection.
$diff = \Drupal::service('config.manager')->diff($active, $sync, $config_name, NULL, 'test');
$edits = $diff->getEdits();
$this->assertYamlEdit($edits, 'foo', 'change', ['foo: bar'], ['foo: baz']);
}
/**
* Helper method to test that an edit is found in the diff of two storages.
*
* @param array $edits
* A list of edits.
* @param string $field
* The field key that is being asserted.
* @param string $type
* The type of edit that is being asserted.
* @param mixed $orig
* (optional) The original value of the edit. If not supplied, assertion
* is skipped.
* @param mixed $closing
* (optional) The closing value of the edit. If not supplied, assertion
* is skipped.
*
* @internal
*/
protected function assertYamlEdit(array $edits, string $field, string $type, $orig = NULL, $closing = NULL): void {
$match = FALSE;
foreach ($edits as $edit) {
// Choose which section to search for the field.
$haystack = $type == 'add' ? $edit->closing : $edit->orig;
// Look through each line and try and find the key.
if (is_array($haystack)) {
foreach ($haystack as $item) {
if (str_starts_with($item, $field . ':')) {
$match = TRUE;
// Assert that the edit is of the type specified.
$this->assertEquals($type, $edit->type, "The {$field} item in the diff is a {$type}");
// If an original value was given, assert that it matches.
if (isset($orig)) {
$this->assertSame($orig, $edit->orig, "The original value for key '{$field}' is correct.");
}
// If a closing value was given, assert that it matches.
if (isset($closing)) {
$this->assertSame($closing, $edit->closing, "The closing value for key '{$field}' is correct.");
}
// Break out of the search entirely.
break 2;
}
}
}
}
// If we didn't match anything, fail.
if (!$match) {
$this->fail("$field edit was not matched");
}
}
}

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the listing of configuration entities.
*
* @group config
*/
class ConfigEntityNormalizeTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['config_test'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(static::$modules);
}
/**
* Tests the normalization of configuration data when saved.
*/
public function testNormalize(): void {
$config_entity = \Drupal::entityTypeManager()->getStorage('config_test')->create(['id' => 'system', 'label' => 'foobar', 'weight' => 1]);
$config_entity->save();
// Modify stored config entity, this is comparable with a schema change.
$config = $this->config('config_test.dynamic.system');
$data = [
'label' => 'foobar',
'additional_key' => TRUE,
] + $config->getRawData();
$config->setData($data)->save();
$this->assertNotSame($config_entity->toArray(), $config->getRawData(), 'Stored config entity is not is equivalent to config schema.');
$config_entity = \Drupal::entityTypeManager()->getStorage('config_test')->load('system');
$config_entity->save();
$config = $this->config('config_test.dynamic.system');
$this->assertSame($config_entity->toArray(), $config->getRawData(), 'Stored config entity is equivalent to config schema.');
}
}

View File

@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\config_entity_static_cache_test\ConfigOverrider;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the entity static cache when used by config entities.
*
* @group config
*/
class ConfigEntityStaticCacheTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'config_test',
'config_entity_static_cache_test',
];
/**
* The type ID of the entity under test.
*
* @var string
*/
protected $entityTypeId;
/**
* The entity ID of the entity under test.
*
* @var string
*/
protected $entityId;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entityTypeId = 'config_test';
$this->entityId = 'test_1';
$this->container->get('entity_type.manager')
->getStorage($this->entityTypeId)
->create(['id' => $this->entityId, 'label' => 'Original label'])
->save();
}
/**
* Tests that the static cache is working.
*/
public function testCacheHit(): void {
$storage = $this->container->get('entity_type.manager')
->getStorage($this->entityTypeId);
$entity_1 = $storage->load($this->entityId);
$entity_2 = $storage->load($this->entityId);
// config_entity_static_cache_test_config_test_load() sets _loadStamp to a
// random string. If they match, it means $entity_2 was retrieved from the
// static cache rather than going through a separate load sequence.
$this->assertSame($entity_1->_loadStamp, $entity_2->_loadStamp);
}
/**
* Tests that the static cache is reset on entity save and delete.
*/
public function testReset(): void {
$storage = $this->container->get('entity_type.manager')
->getStorage($this->entityTypeId);
$entity = $storage->load($this->entityId);
// Ensure loading after a save retrieves the updated entity rather than an
// obsolete cached one.
$entity->label = 'New label';
$entity->save();
$entity = $storage->load($this->entityId);
$this->assertSame('New label', $entity->label);
// Ensure loading after a delete retrieves NULL rather than an obsolete
// cached one.
$entity->delete();
$this->assertNull($storage->load($this->entityId));
}
/**
* Tests that the static cache is sensitive to config overrides.
*/
public function testConfigOverride(): void {
/** @var \Drupal\Core\Config\Entity\ConfigEntityStorage $storage */
$storage = \Drupal::entityTypeManager()->getStorage($this->entityTypeId);
// Prime the cache prior to adding a config override.
$storage->load($this->entityId);
// Add the config override, and ensure that what is loaded is correct
// despite the prior cache priming.
\Drupal::configFactory()->addOverride(new ConfigOverrider());
$entity_override = $storage->load($this->entityId);
$this->assertSame('Overridden label', $entity_override->label);
// Load override free to ensure that loading the config entity again does
// not return the overridden value.
$entity_no_override = $storage->loadOverrideFree($this->entityId);
$this->assertNotSame('Overridden label', $entity_no_override->label);
$this->assertNotSame($entity_override->_loadStamp, $entity_no_override->_loadStamp);
// Reload the entity and ensure the cache is used.
$this->assertSame($entity_no_override->_loadStamp, $storage->loadOverrideFree($this->entityId)->_loadStamp);
// Enable overrides and reload the entity and ensure the cache is used.
$this->assertSame($entity_override->_loadStamp, $storage->load($this->entityId)->_loadStamp);
// Reset the cache, ensure that all variations of this entity are
// invalidated.
$storage->resetCache([$this->entityId]);
$this->assertNotSame($entity_no_override->_loadStamp, $storage->loadOverrideFree($this->entityId)->_loadStamp);
$this->assertNotSame($entity_override->_loadStamp, $storage->load($this->entityId)->_loadStamp);
}
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests configuration entity status functionality.
*
* @group config
*/
class ConfigEntityStatusTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['config_test'];
/**
* Tests the enabling/disabling of entities.
*/
public function testCRUD(): void {
$entity = \Drupal::entityTypeManager()->getStorage('config_test')->create([
'id' => $this->randomMachineName(),
]);
$this->assertTrue($entity->status(), 'Default status is enabled.');
$entity->save();
$this->assertTrue($entity->status(), 'Status is enabled after saving.');
$entity->disable()->save();
$this->assertFalse($entity->status(), 'Entity is disabled after disabling.');
$entity->enable()->save();
$this->assertTrue($entity->status(), 'Entity is enabled after enabling.');
$entity = \Drupal::entityTypeManager()->getStorage('config_test')->load($entity->id());
$this->assertTrue($entity->status(), 'Status is enabled after reload.');
}
}

View File

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\Core\Config\ConfigDuplicateUUIDException;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests configuration entity storage.
*
* @group config
*/
class ConfigEntityStorageTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['config_test'];
/**
* Tests creating configuration entities with changed UUIDs.
*/
public function testUUIDConflict(): void {
$entity_type = 'config_test';
$id = 'test_1';
// Load the original configuration entity.
$storage = $this->container->get('entity_type.manager')
->getStorage($entity_type);
$storage->create(['id' => $id])->save();
$entity = $storage->load($id);
$original_properties = $entity->toArray();
// Override with a new UUID and try to save.
$new_uuid = $this->container->get('uuid')->generate();
$entity->set('uuid', $new_uuid);
try {
$entity->save();
$this->fail('Exception thrown when attempting to save a configuration entity with a UUID that does not match the existing UUID.');
}
catch (ConfigDuplicateUUIDException) {
// Expected exception; just continue testing.
}
// Ensure that the config entity was not corrupted.
$entity = $storage->loadUnchanged($entity->id());
$this->assertSame($original_properties, $entity->toArray());
}
/**
* Tests the hasData() method for config entity storage.
*
* @covers \Drupal\Core\Config\Entity\ConfigEntityStorage::hasData
*/
public function testHasData(): void {
$storage = \Drupal::entityTypeManager()->getStorage('config_test');
$this->assertFalse($storage->hasData());
// Add a test config entity and check again.
$storage->create(['id' => $this->randomMachineName()])->save();
$this->assertTrue($storage->hasData());
}
}

View File

@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\KernelTests\KernelTestBase;
/**
* Unit tests for configuration entity base methods.
*
* @group config
*/
class ConfigEntityUnitTest extends KernelTestBase {
/**
* Exempt from strict schema checking.
*
* @var bool
*
* @see \Drupal\Core\Config\Development\ConfigSchemaChecker
*/
protected $strictConfigSchema = FALSE;
/**
* {@inheritdoc}
*/
protected static $modules = ['config_test'];
/**
* The config_test entity storage.
*
* @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface
*/
protected $storage;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->storage = $this->container->get('entity_type.manager')->getStorage('config_test');
}
/**
* Tests storage methods.
*/
public function testStorageMethods(): void {
$entity_type = \Drupal::entityTypeManager()->getDefinition('config_test');
// Test the static extractID() method.
$expected_id = 'test_id';
$config_name = $entity_type->getConfigPrefix() . '.' . $expected_id;
$storage = $this->storage;
$this->assertSame($expected_id, $storage::getIDFromConfigName($config_name, $entity_type->getConfigPrefix()));
// Create three entities, two with the same style.
$style = $this->randomMachineName(8);
for ($i = 0; $i < 2; $i++) {
$entity = $this->storage->create([
'id' => $this->randomMachineName(),
'label' => $this->randomString(),
'style' => $style,
]);
$entity->save();
}
$entity = $this->storage->create([
'id' => $this->randomMachineName(),
'label' => $this->randomString(),
// Use a different length for the entity to ensure uniqueness.
'style' => $this->randomMachineName(9),
]);
$entity->save();
// Ensure that the configuration entity can be loaded by UUID.
$entity_loaded_by_uuid = \Drupal::service('entity.repository')->loadEntityByUuid($entity_type->id(), $entity->uuid());
if (!$entity_loaded_by_uuid) {
$this->fail(sprintf("Failed to load '%s' entity ID '%s' by UUID '%s'.", $entity_type->id(), $entity->id(), $entity->uuid()));
}
// Compare UUIDs as the objects are not identical since
// $entity->enforceIsNew is FALSE and $entity_loaded_by_uuid->enforceIsNew
// is NULL.
$this->assertSame($entity->uuid(), $entity_loaded_by_uuid->uuid());
$entities = $this->storage->loadByProperties();
$this->assertCount(3, $entities, 'Three entities are loaded when no properties are specified.');
$entities = $this->storage->loadByProperties(['style' => $style]);
$this->assertCount(2, $entities, 'Two entities are loaded when the style property is specified.');
// Assert that both returned entities have a matching style property.
foreach ($entities as $entity) {
$this->assertSame($style, $entity->get('style'), 'The loaded entity has the correct style value specified.');
}
// Test that schema type enforcement can be overridden by trusting the data.
$entity = $this->storage->create([
'id' => $this->randomMachineName(),
'label' => $this->randomString(),
'style' => 999,
]);
$entity->save();
$this->assertSame('999', $entity->style);
$entity->style = 999;
$entity->trustData()->save();
$this->assertSame(999, $entity->style);
$entity->save();
$this->assertSame('999', $entity->style);
}
}

View File

@ -0,0 +1,736 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Config\Schema\Mapping;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\Entity\EntityWithPluginCollectionInterface;
use Drupal\Core\Entity\Plugin\DataType\ConfigEntityAdapter;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManager;
use Drupal\Core\TypedData\Plugin\DataType\LanguageReference;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\Core\Validation\Plugin\Validation\Constraint\FullyValidatableConstraint;
use Drupal\KernelTests\KernelTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
// cspell:ignore kthxbai
/**
* 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;
/**
* Whether a config entity of this type has a label.
*
* Most config entity types ensure their entities have a label. But a few do
* not, typically highly abstract/very low level config entities without a
* strong UI presence. For example: REST resource configuration entities and
* entity view displays.
*
* @var bool
*
* @see \Drupal\Core\Entity\EntityInterface::label()
*/
protected bool $hasLabel = TRUE;
/**
* The config entity mapping properties with >=1 required keys.
*
* All top-level properties of a config entity are guaranteed to be defined
* (since they are defined as properties on the corresponding PHP class). That
* is why they can never trigger "required key" validation errors. Only for
* non-top-level properties can such validation errors be triggered, and hence
* that is only possible on top-level properties of `type: mapping`.
*
* @var string[]
* @see \Drupal\Core\Config\Entity\ConfigEntityType::getPropertiesToExport()
* @see ::testRequiredPropertyKeysMissing()
* @see \Drupal\Core\Validation\Plugin\Validation\Constraint\ValidKeysConstraintValidator
*/
protected static array $propertiesWithRequiredKeys = [];
/**
* The config entity properties whose values are optional (set to NULL).
*
* @var string[]
* @see \Drupal\Core\Config\Entity\ConfigEntityTypeInterface::getPropertiesToExport()
* @see ::testRequiredPropertyValuesMissing()
*/
protected static array $propertiesWithOptionalValues = [
'_core',
'third_party_settings',
];
/**
* {@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();
}
/**
* Ensures that the entity created in ::setUp() has no validation errors.
*/
public function testEntityIsValid(): void {
$this->assertInstanceOf(ConfigEntityInterface::class, $this->entity);
$this->assertValidationErrors([]);
}
/**
* Returns the validation constraints applied to the entity's ID.
*
* If the entity type does not define an ID key, the test will fail. If an ID
* key is defined but is not using the `machine_name` data type, the test will
* be skipped.
*
* @return array[]
* The validation constraint configuration applied to the entity's ID.
*/
protected function getMachineNameConstraints(): array {
$id_key = $this->entity->getEntityType()->getKey('id');
$this->assertNotEmpty($id_key, "The entity under test does not define an ID key.");
$data_definition = $this->entity->getTypedData()
->get($id_key)
->getDataDefinition();
if ($data_definition->getDataType() === 'machine_name') {
return $data_definition->getConstraints();
}
else {
$this->markTestSkipped("The entity's ID key does not use the machine_name data type.");
}
}
/**
* Data provider for ::testInvalidMachineNameCharacters().
*
* @return array[]
* The test cases.
*/
public static function providerInvalidMachineNameCharacters(): array {
return [
'INVALID: space separated' => ['space separated', FALSE],
'INVALID: dash separated' => ['dash-separated', FALSE],
'INVALID: uppercase letters' => ['Uppercase_Letters', FALSE],
'INVALID: period separated' => ['period.separated', FALSE],
'VALID: underscore separated' => ['underscore_separated', TRUE],
];
}
/**
* Tests that the entity's ID is tested for invalid characters.
*
* @param string $machine_name
* A machine name to test.
* @param bool $is_expected_to_be_valid
* Whether this machine name is expected to be considered valid.
*
* @dataProvider providerInvalidMachineNameCharacters
*/
public function testInvalidMachineNameCharacters(string $machine_name, bool $is_expected_to_be_valid): void {
$constraints = $this->getMachineNameConstraints();
$this->assertNotEmpty($constraints['Regex']);
$this->assertIsArray($constraints['Regex']);
$this->assertArrayHasKey('pattern', $constraints['Regex']);
$this->assertIsString($constraints['Regex']['pattern']);
$this->assertArrayHasKey('message', $constraints['Regex']);
$this->assertIsString($constraints['Regex']['message']);
$id_key = $this->entity->getEntityType()->getKey('id');
if ($is_expected_to_be_valid) {
$expected_errors = [];
}
else {
$expected_errors = [$id_key => sprintf('The <em class="placeholder">&quot;%s&quot;</em> machine name is not valid.', $machine_name)];
}
// Config entity IDs are immutable by default.
$expected_errors[''] = "The '$id_key' property cannot be changed.";
$this->entity->set($id_key, $machine_name);
$this->assertValidationErrors($expected_errors);
}
/**
* Tests that the entity ID's length is validated if it is a machine name.
*/
public function testMachineNameLength(string $prefix = ''): void {
$constraints = $this->getMachineNameConstraints();
$max_length = $constraints['Length']['max'];
$this->assertIsInt($max_length);
$this->assertGreaterThan(0, $max_length);
$id_key = $this->entity->getEntityType()->getKey('id');
$expected_errors = [
$id_key => 'This value is too long. It should have <em class="placeholder">' . $max_length . '</em> characters or less.',
// Config entity IDs are immutable by default.
'' => "The '$id_key' property cannot be changed.",
];
$this->entity->set($id_key, $prefix . $this->randomMachineName($max_length + 2));
$this->assertValidationErrors($expected_errors);
}
/**
* Data provider for ::testConfigDependenciesValidation().
*
* @return array[]
* The test cases.
*/
public static 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'],
],
[
'dependencies.fun_stuff' => "'fun_stuff' is not a supported key.",
],
],
'empty string in config dependencies' => [
[
'config' => [''],
],
[
'dependencies.config.0' => [
'This value should not be blank.',
"The '' config does not exist.",
],
],
],
'non-existent config dependency' => [
[
'config' => ['fake_settings'],
],
[
'dependencies.config.0' => "The 'fake_settings' config does not exist.",
],
],
'empty string in module dependencies' => [
[
'module' => [''],
],
[
'dependencies.module.0' => [
'This value should not be blank.',
"Module '' is not installed.",
],
],
],
'invalid module dependency' => [
[
'module' => ['invalid-module-name'],
],
[
'dependencies.module.0' => [
'This value is not a valid extension name.',
"Module 'invalid-module-name' is not installed.",
],
],
],
'non-installed module dependency' => [
[
'module' => ['bad_judgment'],
],
[
'dependencies.module.0' => "Module 'bad_judgment' is not installed.",
],
],
'empty string in theme dependencies' => [
[
'theme' => [''],
],
[
'dependencies.theme.0' => [
'This value should not be blank.',
"Theme '' is not installed.",
],
],
],
'invalid theme dependency' => [
[
'theme' => ['invalid-theme-name'],
],
[
'dependencies.theme.0' => [
'This value is not a valid extension name.',
"Theme 'invalid-theme-name' is not installed.",
],
],
],
'non-installed theme dependency' => [
[
'theme' => ['ugly_theme'],
],
[
'dependencies.theme.0' => "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 array<string, string|string[]> $expected_messages
* The expected validation error messages. Keys are property paths, values
* are the expected messages: a string if a single message is expected, an
* array of strings if multiple are expected.
*
* @dataProvider providerConfigDependenciesValidation
*/
public function testConfigDependenciesValidation(array $dependencies, array $expected_messages): void {
// Add the dependencies we were given to the dependencies that may already
// exist in the entity.
$dependencies = NestedArray::mergeDeep($dependencies, $this->entity->getDependencies());
$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,
]);
// We now expect validation errors not at `dependencies.module.0`, but at
// `dependencies.enforced.module.0`. So reuse the same messages, but perform
// string replacement in the keys.
$expected_enforced_messages = array_combine(
str_replace('dependencies', 'dependencies.enforced', array_keys($expected_messages)),
array_values($expected_messages),
);
$this->assertValidationErrors($expected_enforced_messages);
}
/**
* Tests validation of config entity's label.
*
* @see \Drupal\Core\Entity\EntityInterface::label()
* @see \Drupal\Core\Entity\EntityBase::label()
*/
public function testLabelValidation(): void {
// Some entity types do not have a label.
if (!$this->hasLabel) {
$this->markTestSkipped();
}
if ($this->entity->getEntityType()->getKey('label') === $this->entity->getEntityType()->getKey('id')) {
$this->markTestSkipped('This entity type uses the ID as the label; an entity without a label is hence impossible.');
}
static::setLabel($this->entity, "Multi\nLine");
$this->assertValidationErrors([$this->entity->getEntityType()->getKey('label') => "Labels are not allowed to span multiple lines or contain control characters."]);
}
/**
* Sets the label of the given config entity.
*
* @param \Drupal\Core\Config\Entity\ConfigEntityInterface $entity
* The config entity to modify.
* @param string $label
* The label to set.
*
* @see ::testLabelValidation()
*/
protected static function setLabel(ConfigEntityInterface $entity, string $label): void {
$label_property = $entity->getEntityType()->getKey('label');
if ($label_property === FALSE) {
throw new \LogicException(sprintf('Override %s to allow testing a %s without a label.', __METHOD__, (string) $entity->getEntityType()->getSingularLabel()));
}
$entity->set($label_property, $label);
}
/**
* Asserts a set of validation errors is raised when the entity is validated.
*
* @param array<string, string|string[]> $expected_messages
* The expected validation error messages. Keys are property paths, values
* are the expected messages: a string if a single message is expected, an
* array of strings if multiple are expected.
*/
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) {
$property_path = $violation->getPropertyPath();
if (!isset($actual_messages[$property_path])) {
$actual_messages[$property_path] = (string) $violation->getMessage();
}
else {
// Transform value from string to array.
if (is_string($actual_messages[$property_path])) {
$actual_messages[$property_path] = (array) $actual_messages[$violation->getPropertyPath()];
}
// And append.
$actual_messages[$property_path][] = (string) $violation->getMessage();
}
}
ksort($expected_messages);
ksort($actual_messages);
$this->assertSame($expected_messages, $actual_messages);
}
/**
* Tests that the config entity's langcode is validated.
*/
public function testLangcode(): void {
$this->entity->set('langcode', NULL);
$this->assertValidationErrors([
'langcode' => 'This value should not be null.',
]);
// A langcode from the standard list should always be acceptable.
$standard_languages = LanguageManager::getStandardLanguageList();
$this->assertNotEmpty($standard_languages);
$this->entity->set('langcode', key($standard_languages));
$this->assertValidationErrors([]);
// All special, internal langcodes should be acceptable.
$system_langcodes = [
LanguageInterface::LANGCODE_NOT_SPECIFIED,
LanguageInterface::LANGCODE_NOT_APPLICABLE,
LanguageInterface::LANGCODE_DEFAULT,
LanguageInterface::LANGCODE_SITE_DEFAULT,
LanguageInterface::LANGCODE_SYSTEM,
];
foreach ($system_langcodes as $langcode) {
$this->entity->set('langcode', $langcode);
$this->assertValidationErrors([]);
}
// An invalid langcode should be unacceptable, even if it "looks" right.
$fake_langcode = 'definitely-not-a-language';
$this->assertArrayNotHasKey($fake_langcode, LanguageReference::getAllValidLangcodes());
$this->entity->set('langcode', $fake_langcode);
$this->assertValidationErrors([
'langcode' => 'The value you selected is not a valid choice.',
]);
// If a new configurable language is created with a non-standard langcode,
// it should be acceptable.
$this->enableModules(['language']);
// The language doesn't exist yet, so it shouldn't be a valid choice.
$this->entity->set('langcode', 'kthxbai');
$this->assertValidationErrors([
'langcode' => 'The value you selected is not a valid choice.',
]);
// Once we create the language, it should be a valid choice.
ConfigurableLanguage::createFromLangcode('kthxbai')->save();
$this->assertValidationErrors([]);
}
/**
* Tests that immutable properties cannot be changed.
*
* @param mixed[] $valid_values
* (optional) The values to set for the immutable properties, keyed by name.
* This should be used if the immutable properties can only accept certain
* values, e.g. valid plugin IDs.
*/
public function testImmutableProperties(array $valid_values = []): void {
$constraints = $this->entity->getEntityType()->getConstraints();
$this->assertNotEmpty($constraints['ImmutableProperties'], 'All config entities should have at least one immutable ID property.');
foreach ($constraints['ImmutableProperties'] as $property_name) {
$original_value = $this->entity->get($property_name);
$this->entity->set($property_name, $valid_values[$property_name] ?? $this->randomMachineName());
$this->assertValidationErrors([
'' => "The '$property_name' property cannot be changed.",
]);
$this->entity->set($property_name, $original_value);
}
}
/**
* A property that is required must have a value (i.e. not NULL).
*
* @param string[]|null $additional_expected_validation_errors_when_missing
* Some required config entity properties have additional validation
* constraints that cause additional messages to appear. Keys must be
* config entity properties, values must be arrays as expected by
* ::assertValidationErrors().
*
* @todo Remove this optional parameter in https://www.drupal.org/project/drupal/issues/2820364#comment-15333069
*
* @return void
* No return value.
*/
public function testRequiredPropertyKeysMissing(?array $additional_expected_validation_errors_when_missing = NULL): void {
$config_entity_properties = array_keys($this->entity->getEntityType()->getPropertiesToExport());
if (!empty(array_diff(array_keys($additional_expected_validation_errors_when_missing ?? []), $config_entity_properties))) {
throw new \LogicException(sprintf('The test %s lists `%s` in $additional_expected_validation_errors_when_missing but it is not a property of the `%s` config entity type.',
get_called_class(),
implode(', ', array_diff(array_keys($additional_expected_validation_errors_when_missing), $config_entity_properties)),
$this->entity->getEntityTypeId(),
));
}
$mapping_properties = array_keys(array_filter(
ConfigEntityAdapter::createFromEntity($this->entity)->getProperties(FALSE),
fn (TypedDataInterface $v) => $v instanceof Mapping
));
$required_property_keys = $this->getRequiredPropertyKeys();
if (!$this->isFullyValidatable()) {
$this->assertEmpty($required_property_keys, 'No keys can be required when a config entity type is not fully validatable.');
}
$original_entity = clone $this->entity;
foreach ($mapping_properties as $property) {
$this->entity = clone $original_entity;
$this->entity->set($property, []);
$expected_validation_errors = array_key_exists($property, $required_property_keys)
? [$property => $required_property_keys[$property]]
: [];
$this->assertValidationErrors(($additional_expected_validation_errors_when_missing[$property] ?? []) + $expected_validation_errors);
}
}
/**
* A property that is required must have a value (i.e. not NULL).
*
* @param string[]|null $additional_expected_validation_errors_when_missing
* Some required config entity properties have additional validation
* constraints that cause additional messages to appear. Keys must be
* config entity properties, values must be arrays as expected by
* ::assertValidationErrors().
*
* @todo Remove this optional parameter in https://www.drupal.org/project/drupal/issues/2820364#comment-15333069
*
* @return void
* No return value.
*/
public function testRequiredPropertyValuesMissing(?array $additional_expected_validation_errors_when_missing = NULL): void {
$config_entity_properties = array_keys($this->entity->getEntityType()->getPropertiesToExport());
// Guide developers when $additional_expected_validation_errors_when_missing
// does not contain sensible values.
$non_existing_properties = array_diff(array_keys($additional_expected_validation_errors_when_missing ?? []), $config_entity_properties);
if ($non_existing_properties) {
throw new \LogicException(sprintf('The test %s lists `%s` in $additional_expected_validation_errors_when_missing but it is not a property of the `%s` config entity type.',
__METHOD__,
implode(', ', $non_existing_properties),
$this->entity->getEntityTypeId(),
));
}
$properties_with_optional_values = $this->getPropertiesWithOptionalValues();
// Get the config entity properties that are immutable.
// @see ::testImmutableProperties()
$immutable_properties = $this->entity->getEntityType()->getConstraints()['ImmutableProperties'];
// Config entity properties containing plugin collections are special cases:
// setting them to NULL would cause them to get out of sync with the plugin
// collection.
// @see \Drupal\Core\Config\Entity\ConfigEntityBase::set()
// @see \Drupal\Core\Config\Entity\ConfigEntityBase::preSave()
$plugin_collection_properties = $this->entity instanceof EntityWithPluginCollectionInterface
? array_keys($this->entity->getPluginCollections())
: [];
// To test properties with missing required values, $this->entity must be
// modified to be able to use ::assertValidationErrors(). To allow restoring
// $this->entity to its original value for each tested property, a clone of
// the original entity is needed.
$original_entity = clone $this->entity;
foreach ($config_entity_properties as $property) {
// Do not try to set immutable properties to NULL: their immutability is
// already tested.
// @see ::testImmutableProperties()
if (in_array($property, $immutable_properties, TRUE)) {
continue;
}
// Do not try to set plugin collection properties to NULL.
if (in_array($property, $plugin_collection_properties, TRUE)) {
continue;
}
$this->entity = clone $original_entity;
$this->entity->set($property, NULL);
$expected_validation_errors = in_array($property, $properties_with_optional_values, TRUE)
? []
: [$property => 'This value should not be null.'];
// @see `type: required_label`
// @see \Symfony\Component\Validator\Constraints\NotBlank
if (!$this->isFullyValidatable() && $this->entity->getEntityType()->getKey('label') == $property) {
$expected_validation_errors = [$property => 'This value should not be blank.'];
}
$this->assertValidationErrors(($additional_expected_validation_errors_when_missing[$property] ?? []) + $expected_validation_errors);
}
}
/**
* Whether the tested config entity type is fully validatable.
*
* @return bool
* Whether the tested config entity type is fully validatable.
*/
protected function isFullyValidatable(): bool {
$typed_config = $this->container->get('config.typed');
assert($typed_config instanceof TypedConfigManagerInterface);
// @see \Drupal\Core\Entity\Plugin\DataType\ConfigEntityAdapter::getConfigTypedData()
$config_entity_type_schema_constraints = $typed_config
->createFromNameAndData(
$this->entity->getConfigDependencyName(),
$this->entity->toArray()
)->getConstraints();
foreach ($config_entity_type_schema_constraints as $constraint) {
if ($constraint instanceof FullyValidatableConstraint) {
return TRUE;
}
}
return FALSE;
}
/**
* Determines the config entity mapping properties with required keys.
*
* This refers only to the top-level properties of the config entity which are
* expected to be mappings, and of those mappings, only the ones which have
* required keys.
*
* @return string[]
* An array of key-value pairs, with:
* - keys: names of the config entity properties which are mappings that
* contain required keys.
* - values: the corresponding expected validation error message.
*/
protected function getRequiredPropertyKeys(): array {
// If a config entity type is not fully validatable, no mapping property
// keys are required.
if (!$this->isFullyValidatable()) {
return [];
}
$config_entity_properties = array_keys($this->entity->getEntityType()
->getPropertiesToExport());
// Otherwise, all mapping property keys are required except for those marked
// optional. Rather than inspecting config schema, require authors of tests
// to explicitly list optional properties in a `propertiesWithRequiredKeys`
// property on this class.
// @see \Drupal\KernelTests\Config\Schema\MappingTest::testMappingInterpretation()
$class = static::class;
$properties_with_required_keys = [];
while ($class) {
if (property_exists($class, 'propertiesWithRequiredKeys')) {
$properties_with_required_keys += $class::$propertiesWithRequiredKeys;
}
$class = get_parent_class($class);
}
// Guide developers when $propertiesWithRequiredKeys does not contain
// sensible values.
if (!empty(array_diff(array_keys($properties_with_required_keys), $config_entity_properties))) {
throw new \LogicException(sprintf('The %s test class lists %s in $propertiesWithRequiredKeys but it is not a property of the %s config entity type.',
get_called_class(),
implode(', ', array_diff(array_keys($properties_with_required_keys), $config_entity_properties)),
$this->entity->getEntityTypeId()
));
}
return $properties_with_required_keys;
}
/**
* Determines the config entity properties with optional values.
*
* @return string[]
* The config entity properties whose values are optional.
*/
protected function getPropertiesWithOptionalValues(): array {
$config_entity_properties = array_keys($this->entity->getEntityType()
->getPropertiesToExport());
// If a config entity type is not fully validatable, all properties are
// optional, with the exception of `type: langcode` and
// `type: required_label`.
if (!$this->isFullyValidatable()) {
return array_diff($config_entity_properties, [
// @see `type: langcode`
// @see \Symfony\Component\Validator\Constraints\NotNull
'langcode',
'default_langcode',
// @see `type: required_label`
// @see \Symfony\Component\Validator\Constraints\NotBlank
$this->entity->getEntityType()->getKey('label'),
]);
}
// Otherwise, all properties are required except for those marked
// optional. Rather than inspecting config schema, require authors of tests
// to explicitly list optional properties in a
// `propertiesWithOptionalValues` property on this class.
$class = static::class;
$optional_properties = [];
while ($class) {
if (property_exists($class, 'propertiesWithOptionalValues')) {
$optional_properties = array_merge($optional_properties, $class::$propertiesWithOptionalValues);
}
$class = get_parent_class($class);
}
$optional_properties = array_unique($optional_properties);
// Guide developers when $optionalProperties does not contain sensible
// values.
$non_existing_properties = array_diff($optional_properties, $config_entity_properties);
if ($non_existing_properties) {
throw new \LogicException(sprintf('The %s test class lists %s in $optionalProperties but it is not a property of the %s config entity type.',
static::class,
implode(', ', $non_existing_properties),
$this->entity->getEntityTypeId()
));
}
return $optional_properties;
}
}

View File

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\Core\Config\Config;
use Drupal\Core\Config\ConfigEvents;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests events fired on configuration objects.
*
* @group config
*/
class ConfigEventsTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['config_events_test'];
/**
* Tests configuration events.
*/
public function testConfigEvents(): void {
$name = 'config_events_test.test';
$config = new Config($name, \Drupal::service('config.storage'), \Drupal::service('event_dispatcher'), \Drupal::service('config.typed'));
$config->set('key', 'initial');
$this->assertSame([], \Drupal::state()->get('config_events_test.event', []), 'No events fired by creating a new configuration object');
$config->save();
$event = \Drupal::state()->get('config_events_test.event', []);
$this->assertSame(ConfigEvents::SAVE, $event['event_name']);
$this->assertSame(['key' => 'initial'], $event['current_config_data']);
$this->assertSame(['key' => 'initial'], $event['raw_config_data']);
$this->assertSame([], $event['original_config_data']);
$config->set('key', 'updated')->save();
$event = \Drupal::state()->get('config_events_test.event', []);
$this->assertSame(ConfigEvents::SAVE, $event['event_name']);
$this->assertSame(['key' => 'updated'], $event['current_config_data']);
$this->assertSame(['key' => 'updated'], $event['raw_config_data']);
$this->assertSame(['key' => 'initial'], $event['original_config_data']);
$config->delete();
$event = \Drupal::state()->get('config_events_test.event', []);
$this->assertSame(ConfigEvents::DELETE, $event['event_name']);
$this->assertSame([], $event['current_config_data']);
$this->assertSame([], $event['raw_config_data']);
$this->assertSame(['key' => 'updated'], $event['original_config_data']);
}
/**
* Tests configuration rename event that is fired from the ConfigFactory.
*/
public function testConfigRenameEvent(): void {
$name = 'config_events_test.test';
$new_name = 'config_events_test.test_rename';
$GLOBALS['config'][$name] = ['key' => 'overridden'];
$GLOBALS['config'][$new_name] = ['key' => 'new overridden'];
$config = $this->config($name);
$config->set('key', 'initial')->save();
$event = \Drupal::state()->get('config_events_test.event', []);
$this->assertSame(ConfigEvents::SAVE, $event['event_name']);
$this->assertSame(['key' => 'initial'], $event['current_config_data']);
// Override applies when getting runtime config.
$this->assertEquals($GLOBALS['config'][$name], \Drupal::config($name)->get());
\Drupal::configFactory()->rename($name, $new_name);
$event = \Drupal::state()->get('config_events_test.event', []);
$this->assertSame(ConfigEvents::RENAME, $event['event_name']);
$this->assertSame(['key' => 'new overridden'], $event['current_config_data']);
$this->assertSame(['key' => 'initial'], $event['raw_config_data']);
$this->assertSame(['key' => 'new overridden'], $event['original_config_data']);
}
}

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
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.
*
* @testWith [{}, "system.site", "system.site"]
* [{"prefix": "system."}, "site", "system.site"]
*/
public function testValidation(array $constraint_options, string $value, string $expected_config_name): 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', $constraint_options);
/** @var \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data */
$typed_data = $this->container->get('typed_data_manager');
$data = $typed_data->create($definition, $value);
$violations = $data->validate();
$this->assertCount(1, $violations);
$this->assertSame("The '$expected_config_name' config does not exist.", (string) $violations->get(0)->getMessage());
$this->installConfig('system');
$this->assertCount(0, $data->validate());
// NULL should not trigger a validation error: a value may be nullable.
$data->setValue(NULL);
$this->assertCount(0, $data->validate());
}
}

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests configuration export storage.
*
* @group config
*/
class ConfigExportStorageTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system', 'config_test'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(['system', 'config_test']);
}
/**
* Tests configuration override.
*/
public function testExportStorage(): void {
/** @var \Drupal\Core\Config\StorageInterface $active */
$active = $this->container->get('config.storage');
/** @var \Drupal\Core\Config\StorageInterface $export */
$export = $this->container->get('config.storage.export');
// Test that the active and the export storage contain the same config.
$this->assertNotEmpty($active->listAll());
$this->assertEquals($active->listAll(), $export->listAll());
foreach ($active->listAll() as $name) {
$this->assertEquals($active->read($name), $export->read($name));
}
// Test that the export storage is read-only.
$this->expectException(\BadMethodCallException::class);
$export->deleteAll();
}
}

View File

@ -0,0 +1,237 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Site\Settings;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests reading and writing of configuration files.
*
* @group config
*/
class ConfigFileContentTest extends KernelTestBase {
/**
* Exempt from strict schema checking.
*
* @var bool
*
* @see \Drupal\Core\Config\Development\ConfigSchemaChecker
*/
protected $strictConfigSchema = FALSE;
/**
* Tests setting, writing, and reading of a configuration setting.
*/
public function testReadWriteConfig(): void {
$storage = $this->container->get('config.storage');
$name = 'foo.bar';
$key = 'foo';
$value = 'bar';
$nested_key = 'biff.bang';
$nested_value = 'pow';
$array_key = 'array';
$array_value = [
'foo' => 'bar',
'biff' => [
'bang' => 'pow',
],
];
$casting_array_key = 'casting_array';
$casting_array_false_value_key = 'casting_array.cast.false';
$casting_array_value = [
'cast' => [
'false' => FALSE,
],
];
$nested_array_key = 'nested.array';
$true_key = 'true';
$false_key = 'false';
// Attempt to read non-existing configuration.
$config = $this->config($name);
// Verify a configuration object is returned.
$this->assertEquals($name, $config->getName());
$this->assertNotEmpty($config, 'Config object created.');
// Verify the configuration object is empty.
$this->assertEquals([], $config->get(), 'New config object is empty.');
// Verify nothing was saved.
$data = $storage->read($name);
$this->assertFalse($data);
// Add a top level value.
$config = $this->config($name);
$config->set($key, $value);
// Add a nested value.
$config->set($nested_key, $nested_value);
// Add an array.
$config->set($array_key, $array_value);
// Add a nested array.
$config->set($nested_array_key, $array_value);
// Add a boolean false value. Should get cast to 0.
$config->set($false_key, FALSE);
// Add a boolean true value. Should get cast to 1.
$config->set($true_key, TRUE);
// Add a null value. Should get cast to an empty string.
$config->set('null', NULL);
// Add an array with a nested boolean false that should get cast to 0.
$config->set($casting_array_key, $casting_array_value);
$config->save();
// Verify the database entry exists.
$data = $storage->read($name);
$this->assertNotEmpty($data);
// Read top level value.
$config = $this->config($name);
$this->assertEquals($name, $config->getName());
$this->assertNotEmpty($config, 'Config object created.');
$this->assertEquals('bar', $config->get($key), 'Top level configuration value found.');
// Read nested value.
$this->assertEquals($nested_value, $config->get($nested_key), 'Nested configuration value found.');
// Read array.
$this->assertEquals($array_value, $config->get($array_key), 'Top level array configuration value found.');
// Read nested array.
$this->assertEquals($array_value, $config->get($nested_array_key), 'Nested array configuration value found.');
// Read a top level value that doesn't exist.
$this->assertNull($config->get('i_do_not_exist'), 'Non-existent top level value returned NULL.');
// Read a nested value that doesn't exist.
$this->assertNull($config->get('i.do.not.exist'), 'Non-existent nested value returned NULL.');
// Read false value.
$this->assertFalse($config->get($false_key), "Boolean FALSE value returned the FALSE.");
// Read true value.
$this->assertTrue($config->get($true_key), "Boolean TRUE value returned the TRUE.");
// Read null value.
$this->assertNull($config->get('null'));
// Read false that had been nested in an array value.
$this->assertFalse($config->get($casting_array_false_value_key), "Nested boolean FALSE value returned FALSE.");
// Unset a top level value.
$config->clear($key);
// Unset a nested value.
$config->clear($nested_key);
$config->save();
$config = $this->config($name);
// Read unset top level value.
$this->assertNull($config->get($key), 'Top level value unset.');
// Read unset nested value.
$this->assertNull($config->get($nested_key), 'Nested value unset.');
// Create two new configuration files to test listing.
$config = $this->config('foo.baz');
$config->set($key, $value);
$config->save();
// Test chained set()->save().
$chained_name = 'biff.bang';
$config = $this->config($chained_name);
$config->set($key, $value)->save();
// Verify the database entry exists from a chained save.
$data = $storage->read($chained_name);
$this->assertEquals($config->get(), $data);
// Get file listing for all files starting with 'foo'. Should return
// two elements.
$files = $storage->listAll('foo');
$this->assertCount(2, $files, 'Two files listed with the prefix \'foo\'.');
// Get file listing for all files starting with 'biff'. Should return
// one element.
$files = $storage->listAll('biff');
$this->assertCount(1, $files, 'One file listed with the prefix \'biff\'.');
// Get file listing for all files starting with 'foo.bar'. Should return
// one element.
$files = $storage->listAll('foo.bar');
$this->assertCount(1, $files, 'One file listed with the prefix \'foo.bar\'.');
// Get file listing for all files starting with 'bar'. Should return
// an empty array.
$files = $storage->listAll('bar');
$this->assertEquals([], $files, 'No files listed with the prefix \'bar\'.');
// Delete the configuration.
$config = $this->config($name);
$config->delete();
// Verify the database entry no longer exists.
$data = $storage->read($name);
$this->assertFalse($data);
}
/**
* Tests serialization of configuration to file.
*/
public function testSerialization(): void {
$name = $this->randomMachineName(10) . '.' . $this->randomMachineName(10);
$config_data = [
// Indexed arrays; the order of elements is essential.
'numeric keys' => ['i', 'n', 'd', 'e', 'x', 'e', 'd'],
// Infinitely nested keys using arbitrary element names.
'nested keys' => [
// HTML/XML in values.
'HTML' => '<strong> <bold> <em> <blockquote>',
// UTF-8 in values.
// cspell:disable-next-line
'UTF-8' => 'FrançAIS is ÜBER-åwesome',
// Unicode in keys and values.
// cSpell:disable-next-line
'ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΣὨ' => 'αβγδεζηθικλμνξοσὠ',
],
'invalid xml' => '</title><script type="text/javascript">alert("Title XSS!");</script> & < > " \' ',
];
// Encode and write, and reload and decode the configuration data.
$file_storage = new FileStorage(Settings::get('config_sync_directory'));
$file_storage->write($name, $config_data);
$config_parsed = $file_storage->read($name);
$key = 'numeric keys';
$this->assertSame($config_data[$key], $config_parsed[$key]);
$key = 'nested keys';
$this->assertSame($config_data[$key], $config_parsed[$key]);
$key = 'HTML';
$this->assertSame($config_data['nested keys'][$key], $config_parsed['nested keys'][$key]);
$key = 'UTF-8';
$this->assertSame($config_data['nested keys'][$key], $config_parsed['nested keys'][$key]);
// cSpell:disable-next-line
$key = 'ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΣὨ';
$this->assertSame($config_data['nested keys'][$key], $config_parsed['nested keys'][$key]);
$key = 'invalid xml';
$this->assertSame($config_data[$key], $config_parsed[$key]);
}
}

View File

@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\Core\Config\ConfigImporter;
use Drupal\Core\Config\StorageComparer;
use Drupal\KernelTests\KernelTestBase;
use Drupal\node\Entity\NodeType;
/**
* Tests importing recreated configuration entities.
*
* @group config
*/
class ConfigImportRecreateTest extends KernelTestBase {
/**
* Config Importer object used for testing.
*
* @var \Drupal\Core\Config\ConfigImporter
*/
protected $configImporter;
/**
* {@inheritdoc}
*/
protected static $modules = ['system', 'field', 'text', 'user', 'node'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('node');
$this->installConfig(['system', 'field', 'node']);
$this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync'));
// Set up the ConfigImporter object for testing.
$storage_comparer = new StorageComparer(
$this->container->get('config.storage.sync'),
$this->container->get('config.storage')
);
$this->configImporter = new ConfigImporter(
$storage_comparer->createChangelist(),
$this->container->get('event_dispatcher'),
$this->container->get('config.manager'),
$this->container->get('lock'),
$this->container->get('config.typed'),
$this->container->get('module_handler'),
$this->container->get('module_installer'),
$this->container->get('theme_handler'),
$this->container->get('string_translation'),
$this->container->get('extension.list.module'),
$this->container->get('extension.list.theme')
);
}
/**
* Tests re-creating a config entity with the same name but different UUID.
*/
public function testRecreateEntity(): void {
$type_name = $this->randomMachineName(16);
$content_type = NodeType::create([
'type' => $type_name,
'name' => 'Node type one',
]);
$content_type->save();
node_add_body_field($content_type);
/** @var \Drupal\Core\Config\StorageInterface $active */
$active = $this->container->get('config.storage');
/** @var \Drupal\Core\Config\StorageInterface $sync */
$sync = $this->container->get('config.storage.sync');
$config_name = $content_type->getEntityType()->getConfigPrefix() . '.' . $content_type->id();
$this->copyConfig($active, $sync);
// Delete the content type. This will also delete a field storage, a field,
// an entity view display and an entity form display.
$content_type->delete();
$this->assertFalse($active->exists($config_name), 'Content type\'s old name does not exist active store.');
// Recreate with the same type - this will have a different UUID.
$content_type = NodeType::create([
'type' => $type_name,
'name' => 'Node type two',
]);
$content_type->save();
node_add_body_field($content_type);
$this->configImporter->reset();
// A node type, a field, an entity view display and an entity form display
// will be recreated.
$creates = $this->configImporter->getUnprocessedConfiguration('create');
$deletes = $this->configImporter->getUnprocessedConfiguration('delete');
$this->assertCount(5, $creates, 'There are 5 configuration items to create.');
$this->assertCount(5, $deletes, 'There are 5 configuration items to delete.');
$this->assertCount(0, $this->configImporter->getUnprocessedConfiguration('update'), 'There are no configuration items to update.');
$this->assertSame($creates, array_reverse($deletes), 'Deletes and creates contain the same configuration names in opposite orders due to dependencies.');
$this->configImporter->import();
// Verify that there is nothing more to import.
$this->assertFalse($this->configImporter->reset()->hasUnprocessedConfigurationChanges());
$content_type = NodeType::load($type_name);
$this->assertEquals('Node type one', $content_type->label());
}
}

View File

@ -0,0 +1,162 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\Component\Uuid\Php;
use Drupal\Core\Config\ConfigImporter;
use Drupal\Core\Config\ConfigImporterException;
use Drupal\Core\Config\StorageComparer;
use Drupal\node\Entity\NodeType;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests validating renamed configuration in a configuration import.
*
* @group config
*/
class ConfigImportRenameValidationTest extends KernelTestBase {
/**
* Config Importer object used for testing.
*
* @var \Drupal\Core\Config\ConfigImporter
*/
protected $configImporter;
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'user',
'node',
'field',
'text',
'config_test',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('node');
$this->installConfig(['system', 'field']);
// Set up the ConfigImporter object for testing.
$storage_comparer = new StorageComparer(
$this->container->get('config.storage.sync'),
$this->container->get('config.storage')
);
$this->configImporter = new ConfigImporter(
$storage_comparer->createChangelist(),
$this->container->get('event_dispatcher'),
$this->container->get('config.manager'),
$this->container->get('lock.persistent'),
$this->container->get('config.typed'),
$this->container->get('module_handler'),
$this->container->get('module_installer'),
$this->container->get('theme_handler'),
$this->container->get('string_translation'),
$this->container->get('extension.list.module'),
$this->container->get('extension.list.theme')
);
}
/**
* Tests configuration renaming validation.
*/
public function testRenameValidation(): void {
// Create a test entity.
$test_entity_id = $this->randomMachineName();
$test_entity = \Drupal::entityTypeManager()->getStorage('config_test')->create([
'id' => $test_entity_id,
'label' => $this->randomMachineName(),
]);
$test_entity->save();
$uuid = $test_entity->uuid();
// Stage the test entity and then delete it from the active storage.
$active = $this->container->get('config.storage');
$sync = $this->container->get('config.storage.sync');
$this->copyConfig($active, $sync);
$test_entity->delete();
// Create a content type with a matching UUID in the active storage.
$content_type = NodeType::create([
'type' => $this->randomMachineName(16),
'name' => $this->randomMachineName(),
'uuid' => $uuid,
]);
$content_type->save();
// Confirm that the staged configuration is detected as a rename since the
// UUIDs match.
$this->configImporter->reset();
$expected = [
'node.type.' . $content_type->id() . '::config_test.dynamic.' . $test_entity_id,
];
$renames = $this->configImporter->getUnprocessedConfiguration('rename');
$this->assertSame($expected, $renames);
// Try to import the configuration. We expect an exception to be thrown
// because the staged entity is of a different type.
try {
$this->configImporter->import();
$this->fail('Expected ConfigImporterException thrown when a renamed configuration entity does not match the existing entity type.');
}
catch (ConfigImporterException) {
$expected = [
"Entity type mismatch on rename. node_type not equal to config_test for existing configuration node.type.{$content_type->id()} and staged configuration config_test.dynamic.$test_entity_id.",
];
$this->assertEquals($expected, $this->configImporter->getErrors());
}
}
/**
* Tests configuration renaming validation for simple configuration.
*/
public function testRenameSimpleConfigValidation(): void {
$uuid = new Php();
// Create a simple configuration with a UUID.
$config = $this->config('config_test.new');
$uuid_value = $uuid->generate();
$config->set('uuid', $uuid_value)->save();
$active = $this->container->get('config.storage');
$sync = $this->container->get('config.storage.sync');
$this->copyConfig($active, $sync);
$config->delete();
// Create another simple configuration with the same UUID.
$config = $this->config('config_test.old');
$config->set('uuid', $uuid_value)->save();
// Confirm that the staged configuration is detected as a rename since the
// UUIDs match.
$this->configImporter->reset();
$expected = [
'config_test.old::config_test.new',
];
$renames = $this->configImporter->getUnprocessedConfiguration('rename');
$this->assertSame($expected, $renames);
// Try to import the configuration. We expect an exception to be thrown
// because the rename is for simple configuration.
try {
$this->configImporter->import();
$this->fail('Expected ConfigImporterException thrown when simple configuration is renamed.');
}
catch (ConfigImporterException) {
$expected = [
'Rename operation for simple configuration. Existing configuration config_test.old and staged configuration config_test.new.',
];
$this->assertEquals($expected, $this->configImporter->getErrors());
}
}
}

View File

@ -0,0 +1,200 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\block_content\Entity\BlockContent;
use Drupal\block_content\Entity\BlockContentType;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Core\Block\Plugin\Block\Broken;
use Drupal\Core\Config\ConfigImporter;
use Drupal\Core\Config\StorageComparer;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Logger\RfcLoggerTrait;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\block\Traits\BlockCreationTrait;
use Psr\Log\LoggerInterface;
/**
* Tests importing configuration which has missing content dependencies.
*
* @group config
*/
class ConfigImporterMissingContentTest extends KernelTestBase implements LoggerInterface {
use BlockCreationTrait;
use RfcLoggerTrait;
/**
* The logged messages.
*
* @var string[]
*/
protected $logMessages = [];
/**
* Config Importer object used for testing.
*
* @var \Drupal\Core\Config\ConfigImporter
*/
protected $configImporter;
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'user',
'entity_test',
'config_test',
'config_import_test',
];
/**
* {@inheritdoc}
*/
public function register(ContainerBuilder $container): void {
parent::register($container);
$container->register('logger.ConfigImporterMissingContentTest', __CLASS__)->addTag('logger');
$container->set('logger.ConfigImporterMissingContentTest', $this);
}
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('entity_test');
$this->installEntitySchema('user');
$this->installConfig(['system', 'config_test']);
// Installing config_test's default configuration pollutes the global
// variable being used for recording hook invocations by this test already,
// so it has to be cleared out manually.
unset($GLOBALS['hook_config_test']);
$this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync'));
// Set up the ConfigImporter object for testing.
$storage_comparer = new StorageComparer(
$this->container->get('config.storage.sync'),
$this->container->get('config.storage')
);
$this->configImporter = new ConfigImporter(
$storage_comparer->createChangelist(),
$this->container->get('event_dispatcher'),
$this->container->get('config.manager'),
$this->container->get('lock'),
$this->container->get('config.typed'),
$this->container->get('module_handler'),
$this->container->get('module_installer'),
$this->container->get('theme_handler'),
$this->container->get('string_translation'),
$this->container->get('extension.list.module'),
$this->container->get('extension.list.theme')
);
}
/**
* Tests the missing content event is fired.
*
* @see \Drupal\Core\Config\ConfigImporter::processMissingContent()
* @see \Drupal\config_import_test\EventSubscriber
*/
public function testMissingContent(): void {
\Drupal::state()->set('config_import_test.config_import_missing_content', TRUE);
// Update a configuration entity in the sync directory to have a dependency
// on two content entities that do not exist.
$storage = $this->container->get('config.storage');
$sync = $this->container->get('config.storage.sync');
$entity_one = EntityTest::create(['name' => 'one']);
$entity_two = EntityTest::create(['name' => 'two']);
$entity_three = EntityTest::create(['name' => 'three']);
$dynamic_name = 'config_test.dynamic.dotted.default';
$original_dynamic_data = $storage->read($dynamic_name);
// Entity one will be resolved by
// \Drupal\config_import_test\EventSubscriber::onConfigImporterMissingContentOne().
$original_dynamic_data['dependencies']['content'][] = $entity_one->getConfigDependencyName();
// Entity two will be resolved by
// \Drupal\config_import_test\EventSubscriber::onConfigImporterMissingContentTwo().
$original_dynamic_data['dependencies']['content'][] = $entity_two->getConfigDependencyName();
// Entity three will be resolved by
// \Drupal\Core\Config\Importer\FinalMissingContentSubscriber.
$original_dynamic_data['dependencies']['content'][] = $entity_three->getConfigDependencyName();
$sync->write($dynamic_name, $original_dynamic_data);
// Import.
$this->configImporter->reset()->import();
$this->assertEquals([], $this->configImporter->getErrors(), 'There were no errors during the import.');
$this->assertEquals($entity_one->uuid(), \Drupal::state()->get('config_import_test.config_import_missing_content_one'), 'The missing content event is fired during configuration import.');
$this->assertEquals($entity_two->uuid(), \Drupal::state()->get('config_import_test.config_import_missing_content_two'), 'The missing content event is fired during configuration import.');
$original_dynamic_data = $storage->read($dynamic_name);
$this->assertEquals([$entity_one->getConfigDependencyName(), $entity_two->getConfigDependencyName(), $entity_three->getConfigDependencyName()], $original_dynamic_data['dependencies']['content'], 'The imported configuration entity has the missing content entity dependency.');
}
/**
* Tests the missing content, config import and the block plugin manager.
*
* @see \Drupal\Core\Config\ConfigImporter::processMissingContent()
* @see \Drupal\config_import_test\EventSubscriber
*/
public function testMissingBlockContent(): void {
$this->enableModules([
'block',
'block_content',
'field',
'text',
]);
$this->container->get('theme_installer')->install(['stark']);
$this->installEntitySchema('block_content');
$this->installConfig(['block_content']);
// Create a block content type.
$block_content_type = BlockContentType::create([
'id' => 'test',
'label' => 'Test block content',
'description' => "Provides a block type",
]);
$block_content_type->save();
// And a block content entity.
$block_content = BlockContent::create([
'info' => 'Prototype',
'type' => 'test',
// Set the UUID to make asserting against missing test easy.
'uuid' => '6376f337-fcbf-4b28-b30e-ed5b6932e692',
]);
$block_content->save();
$plugin_id = 'block_content' . PluginBase::DERIVATIVE_SEPARATOR . $block_content->uuid();
$block = $this->placeBlock($plugin_id);
$storage = $this->container->get('config.storage');
$sync = $this->container->get('config.storage.sync');
$this->copyConfig($storage, $sync);
$block->delete();
$block_content->delete();
$block_content_type->delete();
// Import.
$this->logMessages = [];
$config_importer = $this->configImporter();
$config_importer->import();
$this->assertNotContains('The "block_content:6376f337-fcbf-4b28-b30e-ed5b6932e692" block plugin was not found', $this->logMessages);
// Ensure the expected message is generated when creating an instance of the
// block.
$instance = $this->container->get('plugin.manager.block')->createInstance($plugin_id);
$this->assertContains('The "block_content:6376f337-fcbf-4b28-b30e-ed5b6932e692" block plugin was not found', $this->logMessages);
$this->assertInstanceOf(Broken::class, $instance);
}
/**
* {@inheritdoc}
*/
public function log($level, $message, array $context = []): void {
$message_placeholders = \Drupal::service('logger.log_message_parser')->parseMessagePlaceholders($message, $context);
$this->logMessages[] = empty($message_placeholders) ? $message : strtr($message, $message_placeholders);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,284 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\Core\Config\ConfigCollectionEvents;
use Drupal\Core\Config\InstallStorage;
use Drupal\Core\Config\PreExistingConfigException;
use Drupal\Core\Config\UnmetDependenciesException;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests installation of configuration objects in installation functionality.
*
* @group config
* @see \Drupal\Core\Config\ConfigInstaller
*/
class ConfigInstallTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system', 'config_events_test'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Ensure the global variable being asserted by this test does not exist;
// a previous test executed in this request/process might have set it.
unset($GLOBALS['hook_config_test']);
}
/**
* Tests module installation.
*/
public function testModuleInstallation(): void {
$default_config = 'config_test.system';
$default_configuration_entity = 'config_test.dynamic.dotted.default';
// Verify that default module config does not exist before installation yet.
$config = $this->config($default_config);
$this->assertTrue($config->isNew());
$config = $this->config($default_configuration_entity);
$this->assertTrue($config->isNew());
// Ensure that schema provided by modules that are not installed is not
// available.
$this->assertFalse(\Drupal::service('config.typed')->hasConfigSchema('config_schema_test.some_schema'), 'Configuration schema for config_schema_test.some_schema does not exist.');
// Install the test module.
$this->installModules(['config_test']);
// Verify that default module config exists.
\Drupal::configFactory()->reset($default_config);
\Drupal::configFactory()->reset($default_configuration_entity);
$config = $this->config($default_config);
$this->assertFalse($config->isNew());
$config = $this->config($default_configuration_entity);
$this->assertFalse($config->isNew());
// Verify that config_test API hooks were invoked for the dynamic default
// configuration entity.
$this->assertFalse(isset($GLOBALS['hook_config_test']['load']));
$this->assertTrue(isset($GLOBALS['hook_config_test']['presave']));
$this->assertTrue(isset($GLOBALS['hook_config_test']['insert']));
$this->assertFalse(isset($GLOBALS['hook_config_test']['update']));
$this->assertFalse(isset($GLOBALS['hook_config_test']['predelete']));
$this->assertFalse(isset($GLOBALS['hook_config_test']['delete']));
// Install the schema test module.
$this->enableModules(['config_schema_test']);
$this->installConfig(['config_schema_test']);
// After module installation the new schema should exist.
$this->assertTrue(\Drupal::service('config.typed')->hasConfigSchema('config_schema_test.some_schema'), 'Configuration schema for config_schema_test.some_schema exists.');
// Test that uninstalling configuration removes configuration schema.
$this->config('core.extension')->set('module', [])->save();
\Drupal::service('config.manager')->uninstall('module', 'config_test');
$this->assertFalse(\Drupal::service('config.typed')->hasConfigSchema('config_schema_test.some_schema'), 'Configuration schema for config_schema_test.some_schema does not exist.');
}
/**
* Tests that collections are ignored if the event does not return anything.
*/
public function testCollectionInstallationNoCollections(): void {
// Install the test module.
$this->enableModules(['config_collection_install_test']);
$this->installConfig(['config_collection_install_test']);
/** @var \Drupal\Core\Config\StorageInterface $active_storage */
$active_storage = \Drupal::service('config.storage');
$this->assertEquals([], $active_storage->getAllCollectionNames());
}
/**
* Tests config objects in collections are installed as expected.
*/
public function testCollectionInstallationCollections(): void {
$collections = [
'another_collection',
'collection.test1',
'collection.test2',
];
// Set the event listener to return three possible collections.
// @see \Drupal\config_collection_install_test\EventSubscriber
\Drupal::state()->set('config_collection_install_test.collection_names', $collections);
// Install the test module.
$this->enableModules(['config_collection_install_test']);
$this->installConfig(['config_collection_install_test']);
/** @var \Drupal\Core\Config\StorageInterface $active_storage */
$active_storage = \Drupal::service('config.storage');
$this->assertEquals($collections, $active_storage->getAllCollectionNames());
foreach ($collections as $collection) {
$collection_storage = $active_storage->createCollection($collection);
$data = $collection_storage->read('config_collection_install_test.test');
$this->assertEquals($collection, $data['collection']);
}
// Tests that clashing configuration in collections is detected.
try {
\Drupal::service('module_installer')->install(['config_collection_clash_install_test']);
$this->fail('Expected PreExistingConfigException not thrown.');
}
catch (PreExistingConfigException $e) {
$this->assertEquals('config_collection_clash_install_test', $e->getExtension());
$this->assertEquals(['another_collection' => ['config_collection_install_test.test'], 'collection.test1' => ['config_collection_install_test.test'], 'collection.test2' => ['config_collection_install_test.test']], $e->getConfigObjects());
$this->assertEquals('Configuration objects (another_collection/config_collection_install_test.test, collection/test1/config_collection_install_test.test, collection/test2/config_collection_install_test.test) provided by config_collection_clash_install_test already exist in active configuration', $e->getMessage());
}
// Test that the we can use the config installer to install all the
// available default configuration in a particular collection for enabled
// extensions.
\Drupal::service('config.installer')->installCollectionDefaultConfig('entity');
// The 'entity' collection will not exist because the 'config_test' module
// is not enabled.
$this->assertEquals($collections, $active_storage->getAllCollectionNames());
// Enable the 'config_test' module and try again.
$this->enableModules(['config_test']);
\Drupal::service('config.installer')->installCollectionDefaultConfig('entity');
$collections[] = 'entity';
$this->assertEquals($collections, $active_storage->getAllCollectionNames());
$collection_storage = $active_storage->createCollection('entity');
$data = $collection_storage->read('config_test.dynamic.dotted.default');
$this->assertSame(['label' => 'entity'], $data);
// Test that the config manager uninstalls configuration from collections
// as expected.
\Drupal::state()->set('config_events_test.all_events', []);
$this->container->get('config.manager')->uninstall('module', 'config_collection_install_test');
$all_events = \Drupal::state()->get('config_events_test.all_events');
$this->assertArrayHasKey(ConfigCollectionEvents::DELETE_IN_COLLECTION, $all_events);
// The delete-in-collection event has been triggered 3 times.
$this->assertCount(3, $all_events[ConfigCollectionEvents::DELETE_IN_COLLECTION]['config_collection_install_test.test']);
$event_collections = [];
foreach ($all_events[ConfigCollectionEvents::DELETE_IN_COLLECTION]['config_collection_install_test.test'] as $event) {
$event_collections[] = $event['original_config_data']['collection'];
}
$this->assertSame(['another_collection', 'collection.test1', 'collection.test2'], $event_collections);
$this->assertEquals(['entity'], $active_storage->getAllCollectionNames());
\Drupal::state()->set('config_events_test.all_events', []);
$this->container->get('config.manager')->uninstall('module', 'config_test');
$this->assertEquals([], $active_storage->getAllCollectionNames());
$all_events = \Drupal::state()->get('config_events_test.all_events');
$this->assertArrayHasKey(ConfigCollectionEvents::DELETE_IN_COLLECTION, $all_events);
$this->assertCount(1, $all_events[ConfigCollectionEvents::DELETE_IN_COLLECTION]['config_test.dynamic.dotted.default']);
}
/**
* Tests collections which do not support config entities install correctly.
*
* Config entity detection during config installation is done by matching
* config name prefixes. If a collection provides a configuration with a
* matching name but does not support config entities it should be created
* using simple configuration.
*/
public function testCollectionInstallationCollectionConfigEntity(): void {
$collections = [
'entity',
];
\Drupal::state()->set('config_collection_install_test.collection_names', $collections);
// Install the test module.
$this->installModules(['config_test', 'config_collection_install_test']);
/** @var \Drupal\Core\Config\StorageInterface $active_storage */
$active_storage = \Drupal::service('config.storage');
$this->assertEquals($collections, $active_storage->getAllCollectionNames());
$collection_storage = $active_storage->createCollection('entity');
// The config_test.dynamic.dotted.default configuration object saved in the
// active store should be a configuration entity complete with UUID. Because
// the entity collection does not support configuration entities the
// configuration object stored there with the same name should only contain
// a label.
$name = 'config_test.dynamic.dotted.default';
$data = $active_storage->read($name);
$this->assertTrue(isset($data['uuid']));
$data = $collection_storage->read($name);
$this->assertSame(['label' => 'entity'], $data);
}
/**
* Tests the configuration with unmet dependencies is not installed.
*/
public function testDependencyChecking(): void {
$this->installModules(['config_test']);
try {
$this->installModules(['config_install_dependency_test']);
$this->fail('Expected UnmetDependenciesException not thrown.');
}
catch (UnmetDependenciesException $e) {
$this->assertEquals('config_install_dependency_test', $e->getExtension());
$this->assertEquals(['config_test.dynamic.other_module_test_with_dependency' => ['config_other_module_config_test', 'config_test.dynamic.dotted.english']], $e->getConfigObjects());
$this->assertEquals('Configuration objects provided by <em class="placeholder">config_install_dependency_test</em> have unmet dependencies: <em class="placeholder">config_test.dynamic.other_module_test_with_dependency (config_other_module_config_test, config_test.dynamic.dotted.english)</em>', $e->getMessage());
}
try {
$this->installModules(['config_install_double_dependency_test']);
$this->fail('Expected UnmetDependenciesException not thrown.');
}
catch (UnmetDependenciesException $e) {
$this->assertEquals('config_install_double_dependency_test', $e->getExtension());
$this->assertEquals(['config_test.dynamic.other_module_test_with_dependency' => ['config_other_module_config_test', 'config_test.dynamic.dotted.english']], $e->getConfigObjects());
$this->assertEquals('Configuration objects provided by <em class="placeholder">config_install_double_dependency_test</em> have unmet dependencies: <em class="placeholder">config_test.dynamic.other_module_test_with_dependency (config_other_module_config_test, config_test.dynamic.dotted.english)</em>', $e->getMessage());
}
$this->installModules(['config_test_language']);
try {
$this->installModules(['config_install_dependency_test']);
$this->fail('Expected UnmetDependenciesException not thrown.');
}
catch (UnmetDependenciesException $e) {
$this->assertEquals('config_install_dependency_test', $e->getExtension());
$this->assertEquals(['config_test.dynamic.other_module_test_with_dependency' => ['config_other_module_config_test']], $e->getConfigObjects());
$this->assertEquals('Configuration objects provided by <em class="placeholder">config_install_dependency_test</em> have unmet dependencies: <em class="placeholder">config_test.dynamic.other_module_test_with_dependency (config_other_module_config_test)</em>', $e->getMessage());
}
$this->installModules(['config_other_module_config_test']);
$this->installModules(['config_install_dependency_test']);
$entity = \Drupal::entityTypeManager()->getStorage('config_test')->load('other_module_test_with_dependency');
$this->assertNotEmpty($entity, 'The config_test.dynamic.other_module_test_with_dependency configuration has been created during install.');
// Ensure that dependencies can be added during module installation by
// hooks.
$this->assertSame('config_install_dependency_test', $entity->getDependencies()['module'][0]);
}
/**
* Tests imported configuration entities with/without language information.
*/
public function testLanguage(): void {
$this->installModules(['config_test_language']);
// Test imported configuration with implicit language code.
$storage = new InstallStorage();
$data = $storage->read('config_test.dynamic.dotted.english');
$this->assertTrue(!isset($data['langcode']));
$this->assertEquals('en', $this->config('config_test.dynamic.dotted.english')->get('langcode'));
// Test imported configuration with explicit language code.
$data = $storage->read('config_test.dynamic.dotted.french');
$this->assertEquals('fr', $data['langcode']);
$this->assertEquals('fr', $this->config('config_test.dynamic.dotted.french')->get('langcode'));
}
/**
* Tests installing configuration where the filename and ID do not match.
*/
public function testIdMisMatch(): void {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('The configuration name "config_test.dynamic.no_id_match" does not match the ID "does_not_match"');
$this->installModules(['config_test_id_mismatch']);
}
/**
* Installs a module.
*
* @param array $modules
* The module names.
*/
protected function installModules(array $modules): void {
$this->container->get('module_installer')->install($modules);
$this->container = \Drupal::getContainer();
}
}

View File

@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\KernelTests\KernelTestBase;
/**
* Confirm that language overrides work.
*
* @group config
*/
class ConfigLanguageOverrideTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'user',
'language',
'config_test',
'system',
'field',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(['config_test']);
}
/**
* Tests locale override based on language.
*/
public function testConfigLanguageOverride(): void {
// The language module implements a config factory override object that
// overrides configuration when the Language module is enabled. This test
// ensures that English overrides work.
\Drupal::languageManager()->setConfigOverrideLanguage(\Drupal::languageManager()->getLanguage('en'));
$config = \Drupal::config('config_test.system');
$this->assertSame('en bar', $config->get('foo'));
// Ensure that the raw data is not translated.
$raw = $config->getRawData();
$this->assertSame('bar', $raw['foo']);
ConfigurableLanguage::createFromLangcode('fr')->save();
ConfigurableLanguage::createFromLangcode('de')->save();
\Drupal::languageManager()->setConfigOverrideLanguage(\Drupal::languageManager()->getLanguage('fr'));
$config = \Drupal::config('config_test.system');
$this->assertSame('fr bar', $config->get('foo'));
\Drupal::languageManager()->setConfigOverrideLanguage(\Drupal::languageManager()->getLanguage('de'));
$config = \Drupal::config('config_test.system');
$this->assertSame('de bar', $config->get('foo'));
// Test overrides of completely new configuration objects. In normal runtime
// this should only happen for configuration entities as we should not be
// creating simple configuration objects on the fly.
\Drupal::languageManager()
->getLanguageConfigOverride('de', 'config_test.new')
->set('language', 'override')
->save();
$config = \Drupal::config('config_test.new');
$this->assertTrue($config->isNew(), 'The configuration object config_test.new is new');
$this->assertSame('override', $config->get('language'));
$this->assertNull($config->getOriginal('language', FALSE));
// Test how overrides react to base configuration changes. Set up some base
// values.
\Drupal::configFactory()->getEditable('config_test.foo')
->set('value', ['key' => 'original'])
->set('label', 'Original')
// `label` is translatable, hence a `langcode` is required.
// @see \Drupal\Core\Config\Plugin\Validation\Constraint\LangcodeRequiredIfTranslatableValuesConstraint
->set('langcode', 'en')
->save();
\Drupal::languageManager()
->getLanguageConfigOverride('de', 'config_test.foo')
->set('value', ['key' => 'override'])
->set('label', 'Override')
->save();
\Drupal::languageManager()
->getLanguageConfigOverride('fr', 'config_test.foo')
->set('value', ['key' => 'override'])
->save();
\Drupal::configFactory()->clearStaticCache();
$config = \Drupal::config('config_test.foo');
$this->assertSame(['key' => 'override'], $config->get('value'));
// Ensure renaming the config will rename the override.
\Drupal::languageManager()->setConfigOverrideLanguage(\Drupal::languageManager()->getLanguage('en'));
\Drupal::configFactory()->rename('config_test.foo', 'config_test.bar');
$config = \Drupal::config('config_test.bar');
$this->assertEquals(['key' => 'original'], $config->get('value'));
$override = \Drupal::languageManager()->getLanguageConfigOverride('de', 'config_test.foo');
$this->assertTrue($override->isNew());
$this->assertNull($override->get('value'));
$override = \Drupal::languageManager()->getLanguageConfigOverride('de', 'config_test.bar');
$this->assertFalse($override->isNew());
$this->assertEquals(['key' => 'override'], $override->get('value'));
$override = \Drupal::languageManager()->getLanguageConfigOverride('fr', 'config_test.bar');
$this->assertFalse($override->isNew());
$this->assertEquals(['key' => 'override'], $override->get('value'));
// Ensure changing data in the config will update the overrides.
$config = \Drupal::configFactory()->getEditable('config_test.bar')->clear('value.key')->save();
$this->assertEquals([], $config->get('value'));
$override = \Drupal::languageManager()->getLanguageConfigOverride('de', 'config_test.bar');
$this->assertFalse($override->isNew());
$this->assertNull($override->get('value'));
// The French override will become empty and therefore removed.
$override = \Drupal::languageManager()->getLanguageConfigOverride('fr', 'config_test.bar');
$this->assertTrue($override->isNew());
$this->assertNull($override->get('value'));
// Ensure deleting the config will delete the override.
\Drupal::configFactory()->getEditable('config_test.bar')->delete();
$override = \Drupal::languageManager()->getLanguageConfigOverride('de', 'config_test.bar');
$this->assertTrue($override->isNew());
$this->assertNull($override->get('value'));
}
}

View File

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests module overrides of configuration using event subscribers.
*
* @group config
*/
class ConfigModuleOverridesTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system', 'config', 'config_override_test'];
/**
* Tests simple module overrides of configuration using event subscribers.
*/
public function testSimpleModuleOverrides(): void {
$GLOBALS['config_test_run_module_overrides'] = TRUE;
$name = 'system.site';
$overridden_name = 'Wow overridden site name';
$non_overridden_name = 'Wow this name is on disk mkay';
$overridden_slogan = 'Yay for overrides!';
$non_overridden_slogan = 'Yay for defaults!';
$config_factory = $this->container->get('config.factory');
$config_factory
->getEditable($name)
->set('name', $non_overridden_name)
->set('slogan', $non_overridden_slogan)
// `name` and `slogan` are translatable, hence a `langcode` is required.
// @see \Drupal\Core\Config\Plugin\Validation\Constraint\LangcodeRequiredIfTranslatableValuesConstraint
->set('langcode', 'en')
->save();
$this->assertEquals($non_overridden_name, $config_factory->get('system.site')->getOriginal('name', FALSE));
$this->assertEquals($non_overridden_slogan, $config_factory->get('system.site')->getOriginal('slogan', FALSE));
$this->assertEquals($overridden_name, $config_factory->get('system.site')->get('name'));
$this->assertEquals($overridden_slogan, $config_factory->get('system.site')->get('slogan'));
// Test overrides of completely new configuration objects. In normal runtime
// this should only happen for configuration entities as we should not be
// creating simple configuration objects on the fly.
$config = $config_factory->get('config_override_test.new');
$this->assertTrue($config->isNew(), 'The configuration object config_override_test.new is new');
$this->assertSame('override', $config->get('module'));
$this->assertNull($config->getOriginal('module', FALSE));
unset($GLOBALS['config_test_run_module_overrides']);
}
}

View File

@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests configuration overrides via $config in settings.php.
*
* @group config
*/
class ConfigOverrideTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system', 'config_test'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(['system']);
$this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync'));
}
/**
* Tests configuration override.
*/
public function testConfOverride(): void {
$expected_original_data = [
'foo' => 'bar',
'baz' => NULL,
'404' => 'herp',
];
// Set globals before installing to prove that the installed file does not
// contain these values.
$overrides['config_test.system']['foo'] = 'overridden';
$overrides['config_test.system']['baz'] = 'injected';
$overrides['config_test.system']['404'] = 'something';
$GLOBALS['config'] = $overrides;
$this->installConfig(['config_test']);
// Verify that the original configuration data exists. Have to read storage
// directly otherwise overrides will apply.
$active = $this->container->get('config.storage');
$data = $active->read('config_test.system');
$this->assertSame($expected_original_data['foo'], $data['foo']);
$this->assertFalse(isset($data['baz']));
$this->assertSame($expected_original_data['404'], $data['404']);
// Get the configuration object with overrides.
$config = \Drupal::configFactory()->get('config_test.system');
// Verify that it contains the overridden data from $config.
$this->assertSame($overrides['config_test.system']['foo'], $config->get('foo'));
$this->assertSame($overrides['config_test.system']['baz'], $config->get('baz'));
$this->assertSame($overrides['config_test.system']['404'], $config->get('404'));
// Get the configuration object which does not have overrides.
$config = \Drupal::configFactory()->getEditable('config_test.system');
// Verify that it does not contains the overridden data from $config.
$this->assertSame($expected_original_data['foo'], $config->get('foo'));
$this->assertNull($config->get('baz'));
$this->assertSame($expected_original_data['404'], $config->get('404'));
// Set the value for 'baz' (on the original data).
$expected_original_data['baz'] = 'original baz';
$config->set('baz', $expected_original_data['baz']);
// Set the value for '404' (on the original data).
$expected_original_data['404'] = 'original 404';
$config->set('404', $expected_original_data['404']);
// Save the configuration object (having overrides applied).
$config->save();
// Reload it and verify that it still contains overridden data from $config.
$config = \Drupal::config('config_test.system');
$this->assertSame($overrides['config_test.system']['foo'], $config->get('foo'));
$this->assertSame($overrides['config_test.system']['baz'], $config->get('baz'));
$this->assertSame($overrides['config_test.system']['404'], $config->get('404'));
// Verify that raw config data has changed.
$this->assertSame($expected_original_data['foo'], $config->getOriginal('foo', FALSE));
$this->assertSame($expected_original_data['baz'], $config->getOriginal('baz', FALSE));
$this->assertSame($expected_original_data['404'], $config->getOriginal('404', FALSE));
// Write file to sync.
$sync = $this->container->get('config.storage.sync');
$expected_new_data = [
'foo' => 'new_foo',
'404' => 'try again',
];
$sync->write('config_test.system', $expected_new_data);
// Import changed data from sync to active.
$this->configImporter()->import();
$data = $active->read('config_test.system');
// Verify that the new configuration data exists. Have to read storage
// directly otherwise overrides will apply.
$this->assertSame($expected_new_data['foo'], $data['foo']);
$this->assertFalse(isset($data['baz']));
$this->assertSame($expected_new_data['404'], $data['404']);
// Verify that the overrides are still working.
$config = \Drupal::config('config_test.system');
$this->assertSame($overrides['config_test.system']['foo'], $config->get('foo'));
$this->assertSame($overrides['config_test.system']['baz'], $config->get('baz'));
$this->assertSame($overrides['config_test.system']['404'], $config->get('404'));
// Test overrides of completely new configuration objects. In normal runtime
// this should only happen for configuration entities as we should not be
// creating simple configuration objects on the fly.
$GLOBALS['config']['config_test.new']['key'] = 'override';
$config = \Drupal::config('config_test.new');
$this->assertTrue($config->isNew(), 'The configuration object config_test.new is new');
$this->assertSame('override', $config->get('key'));
$config_raw = \Drupal::configFactory()->getEditable('config_test.new');
$this->assertNull($config_raw->get('key'));
$config_raw
->set('key', 'raw')
->set('new_key', 'new_value')
->save();
// Ensure override is preserved but all other data has been updated
// accordingly.
$config = \Drupal::config('config_test.new');
$this->assertFalse($config->isNew(), 'The configuration object config_test.new is not new');
$this->assertSame('override', $config->get('key'));
$this->assertSame('new_value', $config->get('new_key'));
$raw_data = $config->getRawData();
$this->assertSame('raw', $raw_data['key']);
}
}

View File

@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\Core\Language\Language;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests that config overrides are applied in the correct order.
*
* Overrides should be applied in the following order, from lowest priority
* to highest:
* - Language overrides.
* - Module overrides.
* - settings.php overrides.
*
* @group config
*/
class ConfigOverridesPriorityTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'config',
'config_override_test',
'language',
];
/**
* Tests the order of config overrides.
*/
public function testOverridePriorities(): void {
$GLOBALS['config_test_run_module_overrides'] = FALSE;
$non_overridden_mail = 'site@example.com';
$language_overridden_mail = 'french@example.com';
$language_overridden_name = 'French site name';
$module_overridden_name = 'Wow overridden site name';
$non_overridden_name = 'Wow this name is on disk mkay';
$module_overridden_slogan = 'Yay for overrides!';
$non_overridden_slogan = 'Yay for defaults!';
/** @var \Drupal\Core\Config\ConfigFactoryInterface $config_factory */
$config_factory = $this->container->get('config.factory');
$config_factory
->getEditable('system.site')
->set('name', $non_overridden_name)
->set('slogan', $non_overridden_slogan)
->set('mail', $non_overridden_mail)
->set('weight_select_max', 50)
// `name` and `slogan` are translatable, hence a `langcode` is required.
// @see \Drupal\Core\Config\Plugin\Validation\Constraint\LangcodeRequiredIfTranslatableValuesConstraint
->set('langcode', 'en')
->save();
// Ensure that no overrides are applying.
$this->assertEquals($non_overridden_name, $config_factory->get('system.site')->get('name'));
$this->assertEquals($non_overridden_slogan, $config_factory->get('system.site')->get('slogan'));
$this->assertEquals($non_overridden_mail, $config_factory->get('system.site')->get('mail'));
$this->assertEquals(50, $config_factory->get('system.site')->get('weight_select_max'));
// Override using language.
$language = new Language([
'name' => 'French',
'id' => 'fr',
]);
\Drupal::languageManager()->setConfigOverrideLanguage($language);
\Drupal::languageManager()
->getLanguageConfigOverride($language->getId(), 'system.site')
->set('name', $language_overridden_name)
->set('mail', $language_overridden_mail)
->save();
$this->assertEquals($language_overridden_name, $config_factory->get('system.site')->get('name'));
$this->assertEquals($non_overridden_slogan, $config_factory->get('system.site')->get('slogan'));
$this->assertEquals($language_overridden_mail, $config_factory->get('system.site')->get('mail'));
$this->assertEquals(50, $config_factory->get('system.site')->get('weight_select_max'));
// Enable module overrides. Do not override system.site:mail to prove that
// the language override still applies.
$GLOBALS['config_test_run_module_overrides'] = TRUE;
$config_factory->reset('system.site');
$this->assertEquals($module_overridden_name, $config_factory->get('system.site')->get('name'));
$this->assertEquals($module_overridden_slogan, $config_factory->get('system.site')->get('slogan'));
$this->assertEquals($language_overridden_mail, $config_factory->get('system.site')->get('mail'));
$this->assertEquals(50, $config_factory->get('system.site')->get('weight_select_max'));
// Configure a global override to simulate overriding using settings.php. Do
// not override system.site:mail or system.site:slogan to prove that the
// language and module overrides still apply.
$GLOBALS['config']['system.site']['name'] = 'Site name global conf override';
$config_factory->reset('system.site');
$this->assertEquals('Site name global conf override', $config_factory->get('system.site')->get('name'));
$this->assertEquals($module_overridden_slogan, $config_factory->get('system.site')->get('slogan'));
$this->assertEquals($language_overridden_mail, $config_factory->get('system.site')->get('mail'));
$this->assertEquals(50, $config_factory->get('system.site')->get('weight_select_max'));
$this->assertEquals($non_overridden_name, $config_factory->get('system.site')->getOriginal('name', FALSE));
$this->assertEquals($non_overridden_slogan, $config_factory->get('system.site')->getOriginal('slogan', FALSE));
$this->assertEquals($non_overridden_mail, $config_factory->get('system.site')->getOriginal('mail', FALSE));
$this->assertEquals(50, $config_factory->get('system.site')->getOriginal('weight_select_max', FALSE));
unset($GLOBALS['config_test_run_module_overrides']);
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests config schema deprecation.
*
* @group config
* @group legacy
*/
class ConfigSchemaDeprecationTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'config_schema_deprecated_test',
];
/**
* Tests config schema deprecation.
*/
public function testConfigSchemaDeprecation(): void {
$this->expectDeprecation('The \'complex_structure_deprecated\' config schema is deprecated in drupal:9.1.0 and is removed from drupal 10.0.0. Use the \'complex_structure\' config schema instead. See http://drupal.org/node/the-change-notice-nid.');
$config = $this->config('config_schema_deprecated_test.settings');
$config
->set('complex_structure_deprecated.type', 'fruits')
->set('complex_structure_deprecated.products', ['apricot', 'apple'])
->save();
$this->assertSame(['type' => 'fruits', 'products' => ['apricot', 'apple']], $config->get('complex_structure_deprecated'));
}
}

View File

@ -0,0 +1,880 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Config\InstallStorage;
use Drupal\Core\Config\Schema\ConfigSchemaAlterException;
use Drupal\Core\Config\Schema\Ignore;
use Drupal\Core\Config\Schema\Mapping;
use Drupal\Core\Config\Schema\Undefined;
use Drupal\Core\TypedData\Plugin\DataType\StringData;
use Drupal\Core\TypedData\Type\IntegerInterface;
use Drupal\Core\TypedData\Type\StringInterface;
use Drupal\image\ImageEffectInterface;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests schema for configuration objects.
*
* @group config
*/
class ConfigSchemaTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'language',
'field',
'image',
'config_test',
'config_schema_test',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(['system', 'image', 'config_schema_test']);
}
/**
* Tests the basic metadata retrieval layer.
*/
public function testSchemaMapping(): void {
// Nonexistent configuration key will have Undefined as metadata.
$this->assertFalse(\Drupal::service('config.typed')->hasConfigSchema('config_schema_test.no_such_key'));
$definition = \Drupal::service('config.typed')->getDefinition('config_schema_test.no_such_key');
$expected = [];
$expected['label'] = 'Undefined';
$expected['class'] = Undefined::class;
$expected['type'] = 'undefined';
$expected['definition_class'] = '\Drupal\Core\TypedData\DataDefinition';
$expected['unwrap_for_canonical_representation'] = TRUE;
$this->assertEquals($expected, $definition, 'Retrieved the right metadata for nonexistent configuration.');
// Configuration file without schema will return Undefined as well.
$this->assertFalse(\Drupal::service('config.typed')->hasConfigSchema('config_schema_test.no_schema'));
$definition = \Drupal::service('config.typed')->getDefinition('config_schema_test.no_schema');
$this->assertEquals($expected, $definition, 'Retrieved the right metadata for configuration with no schema.');
// Configuration file with only some schema.
$this->assertTrue(\Drupal::service('config.typed')->hasConfigSchema('config_schema_test.some_schema'));
$definition = \Drupal::service('config.typed')->getDefinition('config_schema_test.some_schema');
$expected = [];
$expected['label'] = 'Schema test data';
$expected['class'] = Mapping::class;
$expected['mapping']['langcode']['type'] = 'langcode';
$expected['mapping']['langcode']['requiredKey'] = FALSE;
$expected['mapping']['_core']['type'] = '_core_config_info';
$expected['mapping']['_core']['requiredKey'] = FALSE;
$expected['mapping']['test_item'] = ['label' => 'Test item'];
$expected['mapping']['test_list'] = ['label' => 'Test list'];
$expected['type'] = 'config_schema_test.some_schema';
$expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition';
$expected['unwrap_for_canonical_representation'] = TRUE;
$expected['constraints'] = [
'ValidKeys' => '<infer>',
'LangcodeRequiredIfTranslatableValues' => NULL,
];
$this->assertEquals($expected, $definition, 'Retrieved the right metadata for configuration with only some schema.');
// Check type detection on elements with undefined types.
$config = \Drupal::service('config.typed')->get('config_schema_test.some_schema');
$definition = $config->get('test_item')->getDataDefinition()->toArray();
$expected = [];
$expected['label'] = 'Test item';
$expected['class'] = Undefined::class;
$expected['type'] = 'undefined';
$expected['definition_class'] = '\Drupal\Core\TypedData\DataDefinition';
$expected['unwrap_for_canonical_representation'] = TRUE;
$expected['requiredKey'] = TRUE;
$this->assertEquals($expected, $definition, 'Automatic type detected for a scalar is undefined.');
$definition = $config->get('test_list')->getDataDefinition()->toArray();
$expected = [];
$expected['label'] = 'Test list';
$expected['class'] = Undefined::class;
$expected['type'] = 'undefined';
$expected['definition_class'] = '\Drupal\Core\TypedData\DataDefinition';
$expected['unwrap_for_canonical_representation'] = TRUE;
$expected['requiredKey'] = TRUE;
$this->assertEquals($expected, $definition, 'Automatic type detected for a list is undefined.');
$definition = $config->get('test_no_schema')->getDataDefinition()->toArray();
$expected = [];
$expected['label'] = 'Undefined';
$expected['class'] = Undefined::class;
$expected['type'] = 'undefined';
$expected['definition_class'] = '\Drupal\Core\TypedData\DataDefinition';
$expected['unwrap_for_canonical_representation'] = TRUE;
$this->assertEquals($expected, $definition, 'Automatic type detected for an undefined integer is undefined.');
// Simple case, straight metadata.
$definition = \Drupal::service('config.typed')->getDefinition('system.maintenance');
$expected = [];
$expected['label'] = 'Maintenance mode';
$expected['class'] = Mapping::class;
$expected['mapping']['message'] = [
'label' => 'Message to display when in maintenance mode',
'type' => 'text',
];
$expected['mapping']['langcode'] = [
'type' => 'langcode',
];
$expected['mapping']['langcode']['requiredKey'] = FALSE;
$expected['mapping']['_core']['type'] = '_core_config_info';
$expected['mapping']['_core']['requiredKey'] = FALSE;
$expected['type'] = 'system.maintenance';
$expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition';
$expected['unwrap_for_canonical_representation'] = TRUE;
$expected['constraints'] = [
'ValidKeys' => '<infer>',
'FullyValidatable' => NULL,
'LangcodeRequiredIfTranslatableValues' => NULL,
];
$this->assertEquals($expected, $definition, 'Retrieved the right metadata for system.maintenance');
// Mixed schema with ignore elements.
$definition = \Drupal::service('config.typed')->getDefinition('config_schema_test.ignore');
$expected = [];
$expected['label'] = 'Ignore test';
$expected['class'] = Mapping::class;
$expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition';
$expected['mapping']['langcode'] = [
'type' => 'langcode',
];
$expected['mapping']['langcode']['requiredKey'] = FALSE;
$expected['mapping']['_core']['type'] = '_core_config_info';
$expected['mapping']['_core']['requiredKey'] = FALSE;
$expected['mapping']['label'] = [
'label' => 'Label',
'type' => 'label',
];
$expected['mapping']['irrelevant'] = [
'label' => 'Irrelevant',
'type' => 'ignore',
];
$expected['mapping']['indescribable'] = [
'label' => 'Indescribable',
'type' => 'ignore',
];
$expected['mapping']['weight'] = [
'label' => 'Weight',
'type' => 'weight',
];
$expected['type'] = 'config_schema_test.ignore';
$expected['unwrap_for_canonical_representation'] = TRUE;
$expected['constraints'] = [
'ValidKeys' => '<infer>',
'LangcodeRequiredIfTranslatableValues' => NULL,
];
$this->assertEquals($expected, $definition);
// The ignore elements themselves.
$definition = \Drupal::service('config.typed')->get('config_schema_test.ignore')->get('irrelevant')->getDataDefinition()->toArray();
$expected = [];
$expected['type'] = 'ignore';
$expected['label'] = 'Irrelevant';
$expected['class'] = Ignore::class;
$expected['definition_class'] = '\Drupal\Core\TypedData\DataDefinition';
$expected['unwrap_for_canonical_representation'] = TRUE;
$expected['requiredKey'] = TRUE;
$this->assertEquals($expected, $definition);
$definition = \Drupal::service('config.typed')->get('config_schema_test.ignore')->get('indescribable')->getDataDefinition()->toArray();
$expected['label'] = 'Indescribable';
$this->assertEquals($expected, $definition);
// More complex case, generic type. Metadata for image style.
$definition = \Drupal::service('config.typed')->getDefinition('image.style.large');
$expected = [];
$expected['label'] = 'Image style';
$expected['class'] = Mapping::class;
$expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition';
$expected['unwrap_for_canonical_representation'] = TRUE;
$expected['mapping']['name']['type'] = 'machine_name';
$expected['mapping']['uuid']['type'] = 'uuid';
$expected['mapping']['uuid']['label'] = 'UUID';
$expected['mapping']['langcode']['type'] = 'langcode';
$expected['mapping']['status']['type'] = 'boolean';
$expected['mapping']['status']['label'] = 'Status';
$expected['mapping']['dependencies']['type'] = 'config_dependencies';
$expected['mapping']['dependencies']['label'] = 'Dependencies';
$expected['mapping']['label']['type'] = 'required_label';
$expected['mapping']['label']['label'] = 'Label';
$expected['mapping']['effects']['type'] = 'sequence';
$expected['mapping']['effects']['sequence']['type'] = 'mapping';
$expected['mapping']['effects']['sequence']['mapping']['id']['type'] = 'string';
$expected['mapping']['effects']['sequence']['mapping']['id']['constraints'] = [
'PluginExists' => [
'manager' => 'plugin.manager.image.effect',
'interface' => ImageEffectInterface::class,
],
];
$expected['mapping']['effects']['sequence']['mapping']['data']['type'] = 'image.effect.[%parent.id]';
$expected['mapping']['effects']['sequence']['mapping']['weight']['type'] = 'weight';
$expected['mapping']['effects']['sequence']['mapping']['uuid']['type'] = 'uuid';
$expected['mapping']['third_party_settings']['type'] = 'sequence';
$expected['mapping']['third_party_settings']['label'] = 'Third party settings';
$expected['mapping']['third_party_settings']['sequence']['type'] = '[%parent.%parent.%type].third_party.[%key]';
$expected['mapping']['third_party_settings']['requiredKey'] = FALSE;
$expected['mapping']['_core']['type'] = '_core_config_info';
$expected['mapping']['_core']['requiredKey'] = FALSE;
$expected['type'] = 'image.style.*';
$expected['constraints'] = [
'ValidKeys' => '<infer>',
'FullyValidatable' => NULL,
];
$this->assertEquals($expected, $definition);
// More complex, type based on a complex one.
$definition = \Drupal::service('config.typed')->getDefinition('image.effect.image_scale');
// This should be the schema for image.effect.image_scale.
$expected = [];
$expected['label'] = 'Image scale';
$expected['class'] = Mapping::class;
$expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition';
$expected['unwrap_for_canonical_representation'] = TRUE;
$expected['mapping']['width']['type'] = 'integer';
$expected['mapping']['width']['label'] = 'Width';
$expected['mapping']['width']['nullable'] = TRUE;
$expected['mapping']['width']['constraints'] = ['NotBlank' => ['allowNull' => TRUE]];
$expected['mapping']['height']['type'] = 'integer';
$expected['mapping']['height']['label'] = 'Height';
$expected['mapping']['height']['nullable'] = TRUE;
$expected['mapping']['height']['constraints'] = ['NotBlank' => ['allowNull' => TRUE]];
$expected['mapping']['upscale']['type'] = 'boolean';
$expected['mapping']['upscale']['label'] = 'Upscale';
$expected['type'] = 'image.effect.image_scale';
$expected['constraints'] = [
'ValidKeys' => '<infer>',
'FullyValidatable' => NULL,
];
$this->assertEquals($expected, $definition, 'Retrieved the right metadata for image.effect.image_scale');
// Most complex case, get metadata for actual configuration element.
$effects = \Drupal::service('config.typed')->get('image.style.medium')->get('effects');
$definition = $effects->get('bddf0d06-42f9-4c75-a700-a33cafa25ea0')->get('data')->getDataDefinition()->toArray();
// This should be the schema for image.effect.image_scale, reuse previous
// one.
$expected['type'] = 'image.effect.image_scale';
$expected['mapping']['width']['requiredKey'] = TRUE;
$expected['mapping']['height']['requiredKey'] = TRUE;
$expected['mapping']['upscale']['requiredKey'] = TRUE;
$expected['requiredKey'] = TRUE;
$expected['required'] = TRUE;
$this->assertEquals($expected, $definition, 'Retrieved the right metadata for the first effect of image.style.medium');
$test = \Drupal::service('config.typed')->get('config_test.dynamic.third_party')->get('third_party_settings.config_schema_test');
$definition = $test->getDataDefinition()->toArray();
$expected = [];
$expected['type'] = 'config_test.dynamic.*.third_party.config_schema_test';
$expected['label'] = 'Mapping';
$expected['class'] = Mapping::class;
$expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition';
$expected['unwrap_for_canonical_representation'] = TRUE;
$expected['mapping'] = [
'integer' => ['type' => 'integer', 'requiredKey' => TRUE],
'string' => ['type' => 'string', 'requiredKey' => TRUE],
];
$expected['constraints'] = ['ValidKeys' => '<infer>'];
$this->assertEquals($expected, $definition, 'Retrieved the right metadata for config_test.dynamic.third_party:third_party_settings.config_schema_test');
// More complex, several level deep test.
$definition = \Drupal::service('config.typed')->getDefinition('config_schema_test.some_schema.some_module.section_one.subsection');
// This should be the schema of
// config_schema_test.some_schema.some_module.*.*.
$expected = [];
$expected['label'] = 'Schema multiple filesystem marker test';
$expected['class'] = Mapping::class;
$expected['mapping']['langcode']['type'] = 'langcode';
$expected['mapping']['langcode']['requiredKey'] = FALSE;
$expected['mapping']['_core']['type'] = '_core_config_info';
$expected['mapping']['_core']['requiredKey'] = FALSE;
$expected['mapping']['test_id']['type'] = 'string';
$expected['mapping']['test_id']['label'] = 'ID';
$expected['mapping']['test_description']['type'] = 'text';
$expected['mapping']['test_description']['label'] = 'Description';
$expected['type'] = 'config_schema_test.some_schema.some_module.*.*';
$expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition';
$expected['unwrap_for_canonical_representation'] = TRUE;
$expected['constraints'] = [
'ValidKeys' => '<infer>',
'LangcodeRequiredIfTranslatableValues' => NULL,
];
$this->assertEquals($expected, $definition, 'Retrieved the right metadata for config_schema_test.some_schema.some_module.section_one.subsection');
$definition = \Drupal::service('config.typed')->getDefinition('config_schema_test.some_schema.some_module.section_two.subsection');
// The other file should have the same schema.
$this->assertEquals($expected, $definition, 'Retrieved the right metadata for config_schema_test.some_schema.some_module.section_two.subsection');
}
/**
* Tests metadata retrieval with several levels of %parent indirection.
*/
public function testSchemaMappingWithParents(): void {
$config_data = \Drupal::service('config.typed')->get('config_schema_test.some_schema.with_parents');
// Test fetching parent one level up.
$entry = $config_data->get('one_level');
$definition = $entry->get('test_item')->getDataDefinition()->toArray();
$expected = [
'type' => 'config_schema_test.some_schema.with_parents.key_1',
'label' => 'Test item nested one level',
'class' => StringData::class,
'definition_class' => '\Drupal\Core\TypedData\DataDefinition',
'unwrap_for_canonical_representation' => TRUE,
'requiredKey' => TRUE,
];
$this->assertEquals($expected, $definition);
// Test fetching parent two levels up.
$entry = $config_data->get('two_levels');
$definition = $entry->get('wrapper')->get('test_item')->getDataDefinition()->toArray();
$expected = [
'type' => 'config_schema_test.some_schema.with_parents.key_2',
'label' => 'Test item nested two levels',
'class' => StringData::class,
'definition_class' => '\Drupal\Core\TypedData\DataDefinition',
'unwrap_for_canonical_representation' => TRUE,
'requiredKey' => TRUE,
];
$this->assertEquals($expected, $definition);
// Test fetching parent three levels up.
$entry = $config_data->get('three_levels');
$definition = $entry->get('wrapper_1')->get('wrapper_2')->get('test_item')->getDataDefinition()->toArray();
$expected = [
'type' => 'config_schema_test.some_schema.with_parents.key_3',
'label' => 'Test item nested three levels',
'class' => StringData::class,
'definition_class' => '\Drupal\Core\TypedData\DataDefinition',
'unwrap_for_canonical_representation' => TRUE,
'requiredKey' => TRUE,
];
$this->assertEquals($expected, $definition);
}
/**
* Tests metadata applied to configuration objects.
*/
public function testSchemaData(): void {
// Try a simple property.
$meta = \Drupal::service('config.typed')->get('system.site');
$property = $meta->get('page')->get('front');
$this->assertInstanceOf(StringInterface::class, $property);
$this->assertEquals('/user/login', $property->getValue(), 'Got the right value for page.front data.');
$definition = $property->getDataDefinition();
$this->assertEmpty($definition['translatable'], 'Got the right translatability setting for page.front data.');
// Check nested array of properties.
$list = $meta->get('page')->getElements();
$this->assertCount(3, $list, 'Got a list with the right number of properties for site page data');
$this->assertArrayHasKey('front', $list);
$this->assertArrayHasKey('403', $list);
$this->assertArrayHasKey('404', $list);
$this->assertEquals('/user/login', $list['front']->getValue(), 'Got the right value for page.front data from the list.');
// And test some TypedConfigInterface methods.
$properties = $list;
$this->assertCount(3, $properties, 'Got the right number of properties for site page.');
$this->assertSame($list['front'], $properties['front']);
$values = $meta->get('page')->toArray();
$this->assertCount(3, $values, 'Got the right number of property values for site page.');
$this->assertSame($values['front'], '/user/login');
// Now let's try something more complex, with nested objects.
$wrapper = \Drupal::service('config.typed')->get('image.style.large');
$effects = $wrapper->get('effects');
$this->assertCount(2, $effects->toArray(), 'Got an array with effects for image.style.large data');
foreach ($effects->toArray() as $uuid => $definition) {
$effect = $effects->get($uuid)->getElements();
if ($definition['id'] == 'image_scale') {
$this->assertFalse($effect['data']->isEmpty(), 'Got data for the image scale effect from metadata.');
$this->assertSame('image_scale', $effect['id']->getValue(), 'Got data for the image scale effect from metadata.');
$this->assertInstanceOf(IntegerInterface::class, $effect['data']->get('width'));
$this->assertEquals(480, $effect['data']->get('width')->getValue(), 'Got the right value for the scale effect width.');
}
if ($definition['id'] == 'image_convert') {
$this->assertFalse($effect['data']->isEmpty(), 'Got data for the image convert effect from metadata.');
$this->assertSame('image_convert', $effect['id']->getValue(), 'Got data for the image convert effect from metadata.');
$this->assertSame('webp', $effect['data']->get('extension')->getValue(), 'Got the right value for the convert effect extension.');
}
}
}
/**
* Tests configuration value data type enforcement using schemas.
*/
public function testConfigSaveWithSchema(): void {
$untyped_values = [
// Test a custom type.
'config_schema_test_integer' => '1',
'config_schema_test_integer_empty_string' => '',
'integer' => '100',
'null_integer' => '',
'float' => '3.14',
'null_float' => '',
'string' => 1,
'null_string' => NULL,
'empty_string' => '',
'boolean' => 1,
// If the config schema doesn't have a type it shouldn't be casted.
'no_type' => 1,
'mapping' => [
'string' => 1,
],
'sequence' => [1, 0, 1],
// Not in schema and therefore should be left untouched.
'not_present_in_schema' => TRUE,
];
$untyped_to_typed = $untyped_values;
$typed_values = [
'config_schema_test_integer' => 1,
'config_schema_test_integer_empty_string' => NULL,
'integer' => 100,
'null_integer' => NULL,
'float' => 3.14,
'null_float' => NULL,
'string' => '1',
'null_string' => NULL,
'empty_string' => '',
'boolean' => TRUE,
'no_type' => 1,
'mapping' => [
'string' => '1',
],
'sequence' => [TRUE, FALSE, TRUE],
'not_present_in_schema' => TRUE,
];
// Save config which has a schema that enforces types.
$config_object = $this->config('config_schema_test.schema_data_types');
$config_object
->setData($untyped_to_typed)
->save();
// Ensure the schemaWrapper property is reset after saving to prevent a
// memory leak.
$this->assertNull((new \ReflectionObject($config_object))->getProperty('schemaWrapper')->getValue($config_object));
$this->assertSame($typed_values, $this->config('config_schema_test.schema_data_types')->get());
// Save config which does not have a schema that enforces types.
$this->config('config_schema_test.no_schema_data_types')
->setData($untyped_values)
->save();
$this->assertSame($untyped_values, $this->config('config_schema_test.no_schema_data_types')->get());
// Ensure that configuration objects with keys marked as ignored are not
// changed when saved. The 'config_schema_test.ignore' will have been saved
// during the installation of configuration in the setUp method.
$extension_path = __DIR__ . '/../../../../../modules/config/tests/config_schema_test/';
$install_storage = new FileStorage($extension_path . InstallStorage::CONFIG_INSTALL_DIRECTORY);
$original_data = $install_storage->read('config_schema_test.ignore');
$installed_data = $this->config('config_schema_test.ignore')->get();
unset($installed_data['_core']);
$this->assertSame($original_data, $installed_data);
}
/**
* Test configuration value data type enforcement using schemas.
*/
public function testConfigSaveMappingSort(): void {
// Top level map sorting.
$data = [
'foo' => '1',
'bar' => '2',
];
// Save config which has a schema that enforces types.
$this->config('config_schema_test.schema_mapping_sort')
->setData($data)
->save();
$this->assertSame(['bar' => '2', 'foo' => '1'], $this->config('config_schema_test.schema_mapping_sort')->get());
$this->config('config_schema_test.schema_mapping_sort')->set('map', ['sub_bar' => '2', 'sub_foo' => '1'])->save();
$this->assertSame(['sub_foo' => '1', 'sub_bar' => '2'], $this->config('config_schema_test.schema_mapping_sort')->get('map'));
}
/**
* Tests configuration sequence sorting using schemas.
*/
public function testConfigSaveWithSequenceSorting(): void {
$data = [
'keyed_sort' => [
'b' => '1',
'a' => '2',
],
'no_sort' => [
'b' => '2',
'a' => '1',
],
];
// Save config which has a schema that enforces sorting.
$this->config('config_schema_test.schema_sequence_sort')
->setData($data)
->save();
$this->assertSame(['a' => '2', 'b' => '1'], $this->config('config_schema_test.schema_sequence_sort')->get('keyed_sort'));
$this->assertSame(['b' => '2', 'a' => '1'], $this->config('config_schema_test.schema_sequence_sort')->get('no_sort'));
$data = [
'value_sort' => ['b', 'a'],
'no_sort' => ['b', 'a'],
];
// Save config which has a schema that enforces sorting.
$this->config('config_schema_test.schema_sequence_sort')
->setData($data)
->save();
$this->assertSame(['a', 'b'], $this->config('config_schema_test.schema_sequence_sort')->get('value_sort'));
$this->assertSame(['b', 'a'], $this->config('config_schema_test.schema_sequence_sort')->get('no_sort'));
// Value sort does not preserve keys - this is intentional.
$data = [
'value_sort' => [1 => 'b', 2 => 'a'],
'no_sort' => [1 => 'b', 2 => 'a'],
];
// Save config which has a schema that enforces sorting.
$this->config('config_schema_test.schema_sequence_sort')
->setData($data)
->save();
$this->assertSame(['a', 'b'], $this->config('config_schema_test.schema_sequence_sort')->get('value_sort'));
$this->assertSame([1 => 'b', 2 => 'a'], $this->config('config_schema_test.schema_sequence_sort')->get('no_sort'));
// Test sorts do not destroy complex values.
$data = [
'complex_sort_value' => [['foo' => 'b', 'bar' => 'b'] , ['foo' => 'a', 'bar' => 'a']],
'complex_sort_key' => ['b' => ['foo' => '1', 'bar' => '1'] , 'a' => ['foo' => '2', 'bar' => '2']],
];
$this->config('config_schema_test.schema_sequence_sort')
->setData($data)
->save();
$this->assertSame([['foo' => 'a', 'bar' => 'a'], ['foo' => 'b', 'bar' => 'b']], $this->config('config_schema_test.schema_sequence_sort')->get('complex_sort_value'));
$this->assertSame(['a' => ['foo' => '2', 'bar' => '2'], 'b' => ['foo' => '1', 'bar' => '1']], $this->config('config_schema_test.schema_sequence_sort')->get('complex_sort_key'));
// Swap the previous test scenario around.
$data = [
'complex_sort_value' => ['b' => ['foo' => '1', 'bar' => '1'] , 'a' => ['foo' => '2', 'bar' => '2']],
'complex_sort_key' => [['foo' => 'b', 'bar' => 'b'] , ['foo' => 'a', 'bar' => 'a']],
];
$this->config('config_schema_test.schema_sequence_sort')
->setData($data)
->save();
$this->assertSame([['foo' => '1', 'bar' => '1'], ['foo' => '2', 'bar' => '2']], $this->config('config_schema_test.schema_sequence_sort')->get('complex_sort_value'));
$this->assertSame([['foo' => 'b', 'bar' => 'b'], ['foo' => 'a', 'bar' => 'a']], $this->config('config_schema_test.schema_sequence_sort')->get('complex_sort_key'));
}
/**
* Tests fallback to a greedy wildcard.
*/
public function testSchemaFallback(): void {
$definition = \Drupal::service('config.typed')->getDefinition('config_schema_test.wildcard_fallback.something');
// This should be the schema of config_schema_test.wildcard_fallback.*.
$expected = [];
$expected['label'] = 'Schema wildcard fallback test';
$expected['class'] = Mapping::class;
$expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition';
$expected['unwrap_for_canonical_representation'] = TRUE;
$expected['mapping']['langcode']['type'] = 'langcode';
$expected['mapping']['langcode']['requiredKey'] = FALSE;
$expected['mapping']['_core']['type'] = '_core_config_info';
$expected['mapping']['_core']['requiredKey'] = FALSE;
$expected['mapping']['test_id']['type'] = 'string';
$expected['mapping']['test_id']['label'] = 'ID';
$expected['mapping']['test_description']['type'] = 'text';
$expected['mapping']['test_description']['label'] = 'Description';
$expected['type'] = 'config_schema_test.wildcard_fallback.*';
$expected['constraints'] = [
'ValidKeys' => '<infer>',
'LangcodeRequiredIfTranslatableValues' => NULL,
];
$this->assertEquals($expected, $definition, 'Retrieved the right metadata for config_schema_test.wildcard_fallback.something');
$definition2 = \Drupal::service('config.typed')->getDefinition('config_schema_test.wildcard_fallback.something.something');
// This should be the schema of config_schema_test.wildcard_fallback.* as
// well.
$this->assertSame($definition, $definition2);
}
/**
* Tests use of colons in schema type determination.
*
* @see \Drupal\Core\Config\TypedConfigManager::getFallbackName()
*/
public function testColonsInSchemaTypeDetermination(): void {
$tests = \Drupal::service('config.typed')->get('config_schema_test.plugin_types')->get('tests')->getElements();
$definition = $tests[0]->getDataDefinition()->toArray();
$this->assertEquals('test.plugin_types.boolean', $definition['type']);
$definition = $tests[1]->getDataDefinition()->toArray();
$this->assertEquals('test.plugin_types.boolean:*', $definition['type']);
$definition = $tests[2]->getDataDefinition()->toArray();
$this->assertEquals('test.plugin_types.*', $definition['type']);
$definition = $tests[3]->getDataDefinition()->toArray();
$this->assertEquals('test.plugin_types.*', $definition['type']);
$tests = \Drupal::service('config.typed')->get('config_schema_test.plugin_types')->get('test_with_parents')->getElements();
$definition = $tests[0]->get('settings')->getDataDefinition()->toArray();
$this->assertEquals('test_with_parents.plugin_types.boolean', $definition['type']);
$definition = $tests[1]->get('settings')->getDataDefinition()->toArray();
$this->assertEquals('test_with_parents.plugin_types.boolean:*', $definition['type']);
$definition = $tests[2]->get('settings')->getDataDefinition()->toArray();
$this->assertEquals('test_with_parents.plugin_types.*', $definition['type']);
$definition = $tests[3]->get('settings')->getDataDefinition()->toArray();
$this->assertEquals('test_with_parents.plugin_types.*', $definition['type']);
}
/**
* Tests hook_config_schema_info_alter().
*/
public function testConfigSchemaInfoAlter(): void {
/** @var \Drupal\Core\Config\TypedConfigManagerInterface $typed_config */
$typed_config = \Drupal::service('config.typed');
$typed_config->clearCachedDefinitions();
// Ensure that keys can not be added or removed by
// hook_config_schema_info_alter().
\Drupal::state()->set('config_schema_test_exception_remove', TRUE);
try {
$typed_config->getDefinitions();
$this->fail('Expected ConfigSchemaAlterException thrown.');
}
catch (ConfigSchemaAlterException $e) {
$this->assertEquals('Invoking hook_config_schema_info_alter() has removed (config_schema_test.hook) schema definitions', $e->getMessage());
}
\Drupal::state()->set('config_schema_test_exception_add', TRUE);
try {
$typed_config->getDefinitions();
$this->fail('Expected ConfigSchemaAlterException thrown.');
}
catch (ConfigSchemaAlterException $e) {
$this->assertEquals('Invoking hook_config_schema_info_alter() has added (config_schema_test.hook_added_definition) and removed (config_schema_test.hook) schema definitions', $e->getMessage());
}
\Drupal::state()->set('config_schema_test_exception_remove', FALSE);
try {
$typed_config->getDefinitions();
$this->fail('Expected ConfigSchemaAlterException thrown.');
}
catch (ConfigSchemaAlterException $e) {
$this->assertEquals('Invoking hook_config_schema_info_alter() has added (config_schema_test.hook_added_definition) schema definitions', $e->getMessage());
}
// Tests that hook_config_schema_info_alter() can add additional metadata to
// existing configuration schema.
\Drupal::state()->set('config_schema_test_exception_add', FALSE);
$definitions = $typed_config->getDefinitions();
$this->assertEquals('new schema info', $definitions['config_schema_test.hook']['additional_metadata']);
}
/**
* Tests saving config when the type is wrapped by a dynamic type.
*/
public function testConfigSaveWithWrappingSchema(): void {
$untyped_values = [
'tests' => [
[
'wrapper_value' => 'foo',
'plugin_id' => 'wrapper:foo',
'internal_value' => 100,
],
],
];
$typed_values = [
'tests' => [
[
'plugin_id' => 'wrapper:foo',
'internal_value' => '100',
'wrapper_value' => 'foo',
],
],
];
// Save config which has a schema that enforces types.
\Drupal::configFactory()->getEditable('wrapping.config_schema_test.plugin_types')
->setData($untyped_values)
->save();
$this->assertSame($typed_values, \Drupal::config('wrapping.config_schema_test.plugin_types')->get());
}
/**
* Tests dynamic config schema type with multiple sub-key references.
*/
public function testConfigSaveWithWrappingSchemaDoubleBrackets(): void {
$untyped_values = [
'tests' => [
[
'wrapper_value' => 'foo',
'foo' => 'turtle',
'bar' => 'horse',
// Converted to a string by 'test.double_brackets.turtle.horse'
// schema.
'another_key' => '100',
],
],
];
$typed_values = [
'tests' => [
[
'another_key' => 100,
'foo' => 'turtle',
'bar' => 'horse',
'wrapper_value' => 'foo',
],
],
];
// Save config which has a schema that enforces types.
\Drupal::configFactory()->getEditable('wrapping.config_schema_test.double_brackets')
->setData($untyped_values)
->save();
// TRICKY: https://www.drupal.org/project/drupal/issues/2663410 introduced a
// bug that made TypedConfigManager sensitive to cache pollution. Saving
// config triggers validation, which in turn triggers that cache pollution
// bug. This is a work-around.
// @todo Remove in https://www.drupal.org/project/drupal/issues/3400181
\Drupal::service('config.typed')->clearCachedDefinitions();
$this->assertSame($typed_values, \Drupal::config('wrapping.config_schema_test.double_brackets')->get());
$tests = \Drupal::service('config.typed')->get('wrapping.config_schema_test.double_brackets')->get('tests')->getElements();
$definition = $tests[0]->getDataDefinition()->toArray();
$this->assertEquals('wrapping.test.double_brackets.*||test.double_brackets.turtle.horse', $definition['type']);
$untyped_values = [
'tests' => [
[
'wrapper_value' => 'foo',
'foo' => 'cat',
'bar' => 'dog',
// Converted to a string by 'test.double_brackets.cat.dog' schema.
'another_key' => 100,
],
],
];
$typed_values = [
'tests' => [
[
'another_key' => '100',
'foo' => 'cat',
'bar' => 'dog',
'wrapper_value' => 'foo',
],
],
];
// Save config which has a schema that enforces types.
\Drupal::configFactory()->getEditable('wrapping.config_schema_test.double_brackets')
->setData($untyped_values)
->save();
// TRICKY: https://www.drupal.org/project/drupal/issues/2663410 introduced a
// bug that made TypedConfigManager sensitive to cache pollution. Saving
// config in a test triggers the schema checking and validation logic from
// \Drupal\Core\Config\Development\ConfigSchemaChecker , which in turn
// triggers that cache pollution bug. This is a work-around.
// @todo Remove in https://www.drupal.org/project/drupal/issues/3400181
\Drupal::service('config.typed')->clearCachedDefinitions();
$this->assertSame($typed_values, \Drupal::config('wrapping.config_schema_test.double_brackets')->get());
$tests = \Drupal::service('config.typed')->get('wrapping.config_schema_test.double_brackets')->get('tests')->getElements();
$definition = $tests[0]->getDataDefinition()->toArray();
$this->assertEquals('wrapping.test.double_brackets.*||test.double_brackets.cat.dog', $definition['type']);
// Combine everything in a single save.
$typed_values = [
'tests' => [
[
'another_key' => 100,
'foo' => 'cat',
'bar' => 'dog',
'wrapper_value' => 'foo',
],
[
'another_key' => '100',
'foo' => 'turtle',
'bar' => 'horse',
'wrapper_value' => 'foo',
],
],
];
\Drupal::configFactory()->getEditable('wrapping.config_schema_test.double_brackets')
->setData($typed_values)
->save();
// TRICKY: https://www.drupal.org/project/drupal/issues/2663410 introduced a
// bug that made TypedConfigManager sensitive to cache pollution. Saving
// config in a test triggers the schema checking and validation logic from
// \Drupal\Core\Config\Development\ConfigSchemaChecker , which in turn
// triggers that cache pollution bug. This is a work-around.
// @todo Remove in https://www.drupal.org/project/drupal/issues/3400181
\Drupal::service('config.typed')->clearCachedDefinitions();
$tests = \Drupal::service('config.typed')->get('wrapping.config_schema_test.double_brackets')->get('tests')->getElements();
$definition = $tests[0]->getDataDefinition()->toArray();
$this->assertEquals('wrapping.test.double_brackets.*||test.double_brackets.cat.dog', $definition['type']);
$definition = $tests[1]->getDataDefinition()->toArray();
$this->assertEquals('wrapping.test.double_brackets.*||test.double_brackets.turtle.horse', $definition['type']);
$typed_values = [
'tests' => [
[
'id' => 'cat:persian.dog',
'foo' => 'cat',
'bar' => 'dog',
'breed' => 'persian',
],
],
];
\Drupal::configFactory()->getEditable('wrapping.config_schema_test.other_double_brackets')
->setData($typed_values)
->save();
// TRICKY: https://www.drupal.org/project/drupal/issues/2663410 introduced a
// bug that made TypedConfigManager sensitive to cache pollution. Saving
// config in a test triggers the schema checking and validation logic from
// \Drupal\Core\Config\Development\ConfigSchemaChecker , which in turn
// triggers that cache pollution bug. This is a work-around.
// @todo Remove in https://www.drupal.org/project/drupal/issues/3400181
\Drupal::service('config.typed')->clearCachedDefinitions();
$tests = \Drupal::service('config.typed')->get('wrapping.config_schema_test.other_double_brackets')->get('tests')->getElements();
$definition = $tests[0]->getDataDefinition()->toArray();
// Check that definition type is a merge of the expected types.
$this->assertEquals('wrapping.test.other_double_brackets.*||test.double_brackets.cat:*.*', $definition['type']);
// Check that breed was inherited from parent definition.
$this->assertEquals([
'type' => 'string',
'requiredKey' => TRUE,
], $definition['mapping']['breed']);
}
/**
* Tests exception is thrown for the root object.
*/
public function testLangcodeRequiredIfTranslatableValuesConstraintError(): void {
$config = \Drupal::configFactory()->getEditable('config_test.foo');
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('The LangcodeRequiredIfTranslatableValues constraint is applied to \'config_test.foo::broken_langcode_required\'. This constraint can only operate on the root object being validated.');
$config
->set('broken_langcode_required.foo', 'bar')
->save();
}
}

View File

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\Core\Config\StorageComparer;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests config snapshot creation and updating.
*
* @group config
*/
class ConfigSnapshotTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['config_test', 'system'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(['system']);
// Update the config snapshot. This allows the parent::setUp() to write
// configuration files.
\Drupal::service('config.manager')->createSnapshot(\Drupal::service('config.storage'), \Drupal::service('config.storage.snapshot'));
$this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync'));
}
/**
* Tests config snapshot creation and updating.
*/
public function testSnapshot(): void {
$active = $this->container->get('config.storage');
$sync = $this->container->get('config.storage.sync');
$snapshot = $this->container->get('config.storage.snapshot');
$config_name = 'config_test.system';
$config_key = 'foo';
$new_data = 'foobar';
$active_snapshot_comparer = new StorageComparer($active, $snapshot);
$sync_snapshot_comparer = new StorageComparer($sync, $snapshot);
// Verify that we have an initial snapshot that matches the active
// configuration. This has to be true as no config should be installed.
$this->assertFalse($active_snapshot_comparer->createChangelist()->hasChanges());
// Install the default config.
$this->installConfig(['config_test']);
// Although we have imported config this has not affected the snapshot.
$this->assertTrue($active_snapshot_comparer->reset()->hasChanges());
// Update the config snapshot.
\Drupal::service('config.manager')->createSnapshot($active, $snapshot);
// The snapshot and active config should now contain the same config
// objects.
$this->assertFalse($active_snapshot_comparer->reset()->hasChanges());
// Change a configuration value in sync.
$sync_data = $this->config($config_name)->get();
$sync_data[$config_key] = $new_data;
$sync->write($config_name, $sync_data);
// Verify that active and snapshot match, and that sync doesn't match
// active.
$this->assertFalse($active_snapshot_comparer->reset()->hasChanges());
$this->assertTrue($sync_snapshot_comparer->createChangelist()->hasChanges());
// Import changed data from sync to active.
$this->configImporter()->import();
// Verify changed config was properly imported.
\Drupal::configFactory()->reset($config_name);
$this->assertSame($new_data, $this->config($config_name)->get($config_key));
// Verify that a new snapshot was created which and that it matches
// the active config.
$this->assertFalse($active_snapshot_comparer->reset()->hasChanges());
}
}

View File

@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config\Entity;
use Drupal\Core\Config\Entity\ConfigEntityUpdater;
use Drupal\Core\Site\Settings;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests \Drupal\Core\Config\Entity\ConfigEntityUpdater.
*
* @coversDefaultClass \Drupal\Core\Config\Entity\ConfigEntityUpdater
* @group config
*/
class ConfigEntityUpdaterTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['config_test', 'system'];
/**
* @covers ::update
*/
public function testUpdate(): void {
// Create some entities to update.
$storage = $this->container->get('entity_type.manager')->getStorage('config_test');
for ($i = 0; $i < 15; $i++) {
$entity_id = 'config_test_' . $i;
$storage->create(['id' => $entity_id, 'label' => $entity_id])->save();
}
// Set up the updater.
$sandbox = [];
$settings = Settings::getInstance() ? Settings::getAll() : [];
$settings['entity_update_batch_size'] = 10;
new Settings($settings);
$updater = $this->container->get('class_resolver')->getInstanceFromDefinition(ConfigEntityUpdater::class);
$callback = function ($config_entity) {
/** @var \Drupal\config_test\Entity\ConfigTest $config_entity */
$number = (int) str_replace('config_test_', '', $config_entity->id());
// Only update even numbered entities.
if ($number % 2 == 0) {
$config_entity->set('label', $config_entity->label . ' (updated)');
return TRUE;
}
return FALSE;
};
// This should run against the first 10 entities. The even numbered labels
// will have been updated.
$updater->update($sandbox, 'config_test', $callback);
$entities = $storage->loadMultiple();
$this->assertEquals('config_test_8 (updated)', $entities['config_test_8']->label());
$this->assertEquals('config_test_9', $entities['config_test_9']->label());
$this->assertEquals('config_test_10', $entities['config_test_10']->label());
$this->assertEquals('config_test_14', $entities['config_test_14']->label());
$this->assertEquals(15, $sandbox['config_entity_updater']['count']);
$this->assertEquals('config_test', $sandbox['config_entity_updater']['entity_type']);
$this->assertCount(5, $sandbox['config_entity_updater']['entities']);
$this->assertEquals(10 / 15, $sandbox['#finished']);
// Update the rest.
$updater->update($sandbox, 'config_test', $callback);
$entities = $storage->loadMultiple();
$this->assertEquals('config_test_8 (updated)', $entities['config_test_8']->label());
$this->assertEquals('config_test_9', $entities['config_test_9']->label());
$this->assertEquals('config_test_10 (updated)', $entities['config_test_10']->label());
$this->assertEquals('config_test_14 (updated)', $entities['config_test_14']->label());
$this->assertEquals(1, $sandbox['#finished']);
$this->assertCount(0, $sandbox['config_entity_updater']['entities']);
}
/**
* @covers ::update
*/
public function testUpdateDefaultCallback(): void {
// Create some entities to update.
$storage = $this->container->get('entity_type.manager')->getStorage('config_test');
for ($i = 0; $i < 15; $i++) {
$entity_id = 'config_test_' . $i;
$storage->create(['id' => $entity_id, 'label' => $entity_id])->save();
}
// Set up the updater.
$sandbox = [];
$settings = Settings::getInstance() ? Settings::getAll() : [];
$settings['entity_update_batch_size'] = 9;
new Settings($settings);
$updater = $this->container->get('class_resolver')->getInstanceFromDefinition(ConfigEntityUpdater::class);
// Cause a dependency to be added during an update.
\Drupal::state()->set('config_test_new_dependency', 'system');
// This should run against the first 10 entities.
$updater->update($sandbox, 'config_test');
$entities = $storage->loadMultiple();
$this->assertEquals(['system'], $entities['config_test_7']->getDependencies()['module']);
$this->assertEquals(['system'], $entities['config_test_8']->getDependencies()['module']);
$this->assertEquals([], $entities['config_test_9']->getDependencies());
$this->assertEquals([], $entities['config_test_14']->getDependencies());
$this->assertEquals(15, $sandbox['config_entity_updater']['count']);
$this->assertCount(6, $sandbox['config_entity_updater']['entities']);
$this->assertEquals(9 / 15, $sandbox['#finished']);
// Update the rest.
$updater->update($sandbox, 'config_test');
$entities = $storage->loadMultiple();
$this->assertEquals(['system'], $entities['config_test_9']->getDependencies()['module']);
$this->assertEquals(['system'], $entities['config_test_14']->getDependencies()['module']);
$this->assertEquals(1, $sandbox['#finished']);
$this->assertCount(0, $sandbox['config_entity_updater']['entities']);
}
/**
* @covers ::update
*/
public function testUpdateException(): void {
$this->enableModules(['entity_test']);
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('The provided entity type ID \'entity_test_mul_changed\' is not a configuration entity type');
$updater = $this->container->get('class_resolver')->getInstanceFromDefinition(ConfigEntityUpdater::class);
$sandbox = [];
$updater->update($sandbox, 'entity_test_mul_changed');
}
/**
* @covers ::update
*/
public function testUpdateOncePerUpdateException(): void {
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Updating multiple entity types in the same update function is not supported');
$updater = $this->container->get('class_resolver')->getInstanceFromDefinition(ConfigEntityUpdater::class);
$sandbox = [];
$updater->update($sandbox, 'config_test');
$updater->update($sandbox, 'config_query_test');
}
}

View File

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests ExcludedModulesEventSubscriber.
*
* @group config
*/
class ExcludedModulesEventSubscriberTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'config_test',
'config_exclude_test',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(['system', 'config_test', 'config_exclude_test']);
$this->setSetting('config_exclude_modules', ['config_test']);
}
/**
* Tests excluding modules from the config export.
*/
public function testExcludedModules(): void {
// Assert that config_test is in the active config.
$active = $this->container->get('config.storage');
$this->assertNotEmpty($active->listAll('config_test.'));
$this->assertNotEmpty($active->listAll('system.'));
$this->assertArrayHasKey('config_test', $active->read('core.extension')['module']);
$collection = $this->randomMachineName();
foreach ($active->listAll() as $config) {
$active->createCollection($collection)->write($config, $active->read($config));
}
// Assert that config_test is not in the export storage.
$export = $this->container->get('config.storage.export');
$this->assertEmpty($export->listAll('config_test.'));
$this->assertNotEmpty($export->listAll('system.'));
$this->assertEmpty($export->createCollection($collection)->listAll('config_test.'));
$this->assertNotEmpty($export->createCollection($collection)->listAll('system.'));
$this->assertArrayNotHasKey('config_test', $export->read('core.extension')['module']);
// The config_exclude_test is not excluded but the menu it installs are.
$this->assertArrayHasKey('config_exclude_test', $export->read('core.extension')['module']);
$this->assertFalse($export->exists('system.menu.exclude_test'));
$this->assertFalse($export->exists('system.menu.indirect_exclude_test'));
// Assert that config_test is again in the import storage.
$import = $this->container->get('config.import_transformer')->transform($export);
$this->assertNotEmpty($import->listAll('config_test.'));
$this->assertNotEmpty($import->listAll('system.'));
$this->assertNotEmpty($import->createCollection($collection)->listAll('config_test.'));
$this->assertNotEmpty($import->createCollection($collection)->listAll('system.'));
$this->assertArrayHasKey('config_test', $import->read('core.extension')['module']);
$this->assertArrayHasKey('config_exclude_test', $import->read('core.extension')['module']);
$this->assertTrue($import->exists('system.menu.exclude-test'));
$this->assertTrue($import->exists('system.menu.indirect-exclude-test'));
$this->assertEquals($active->read('core.extension'), $import->read('core.extension'));
$this->assertEquals($active->listAll(), $import->listAll());
foreach ($active->listAll() as $config) {
$this->assertEquals($active->read($config), $import->read($config));
}
// When the settings are changed, the next request will get the export
// storage without the config_test excluded.
$this->setSetting('config_exclude_modules', []);
// We rebuild the container to simulate a new request. The managed storage
// gets the storage from the manager only once.
$this->container->get('kernel')->rebuildContainer();
$export = $this->container->get('config.storage.export');
$this->assertArrayHasKey('config_test', $export->read('core.extension')['module']);
}
}

View File

@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\Core\Config\ExportStorageManager;
use Drupal\Core\Config\StorageTransformerException;
use Drupal\Core\Lock\NullLockBackend;
use Drupal\KernelTests\KernelTestBase;
// cspell:ignore arrr
/**
* Tests the export storage manager.
*
* @group config
*/
class ExportStorageManagerTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'config_transformer_test',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(['system']);
}
/**
* Tests getting the export storage.
*/
public function testGetStorage(): void {
// Get the raw system.site config and set it in the sync storage.
$rawConfig = $this->config('system.site')->getRawData();
$this->container->get('config.storage.sync')->write('system.site', $rawConfig);
// The export storage manager under test.
$manager = new ExportStorageManager(
$this->container->get('config.storage'),
$this->container->get('database'),
$this->container->get('event_dispatcher'),
new NullLockBackend()
);
$storage = $manager->getStorage();
$exported = $storage->read('system.site');
// The test subscriber adds "Arrr" to the slogan of the sync config.
$this->assertEquals($rawConfig['name'], $exported['name']);
$this->assertEquals($rawConfig['slogan'] . ' Arrr', $exported['slogan']);
// Save the config to active storage so that the transformer can alter it.
$this->config('system.site')
->set('name', 'New name')
->set('slogan', 'New slogan')
->save();
// Get the storage again.
$storage = $manager->getStorage();
$exported = $storage->read('system.site');
// The test subscriber adds "Arrr" to the slogan of the sync config.
$this->assertEquals('New name', $exported['name']);
$this->assertEquals($rawConfig['slogan'] . ' Arrr', $exported['slogan']);
// Change what the transformer does without changing anything else to assert
// that the event is dispatched every time the storage is needed.
$this->container->get('state')->set('config_transform_test_mail', 'config@drupal.example');
$storage = $manager->getStorage();
$exported = $storage->read('system.site');
// The mail is still set to the value from the beginning.
$this->assertEquals('config@drupal.example', $exported['mail']);
}
/**
* Tests the export storage when it is locked.
*/
public function testGetStorageLock(): void {
$lock = $this->createMock('Drupal\Core\Lock\LockBackendInterface');
$lock->expects($this->exactly(2))
->method('acquire')
->with(ExportStorageManager::LOCK_NAME)
->willReturn(FALSE);
$lock->expects($this->once())
->method('wait')
->with(ExportStorageManager::LOCK_NAME);
// The export storage manager under test.
$manager = new ExportStorageManager(
$this->container->get('config.storage'),
$this->container->get('database'),
$this->container->get('event_dispatcher'),
$lock
);
$this->expectException(StorageTransformerException::class);
$this->expectExceptionMessage("Cannot acquire config export transformer lock.");
$manager->getStorage();
}
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\Core\Config\ConfigDirectoryNotDefinedException;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Config\FileStorageFactory;
use Drupal\Core\Site\Settings;
use Drupal\KernelTests\KernelTestBase;
/**
* @coversDefaultClass \Drupal\Core\Config\FileStorageFactory
* @group config
*/
class FileStorageFactoryTest extends KernelTestBase {
/**
* @covers ::getSync
*/
public function testGetSync(): void {
// Write some random data to the sync storage.
$name = $this->randomMachineName();
$data = (array) $this->getRandomGenerator()->object();
$storage = new FileStorage(Settings::get('config_sync_directory'));
$storage->write($name, $data);
// Get the sync storage and read from it.
$sync = FileStorageFactory::getSync();
$this->assertEquals($data, $sync->read($name));
// Unset the sync directory setting.
$settings = Settings::getInstance() ? Settings::getAll() : [];
unset($settings['config_sync_directory']);
new Settings($settings);
// On an empty settings there is an exception thrown.
$this->expectException(ConfigDirectoryNotDefinedException::class);
$this->expectExceptionMessage('The config sync directory is not defined in $settings["config_sync_directory"]');
FileStorageFactory::getSync();
}
}

View File

@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\Core\Config\ConfigImporter;
use Drupal\Core\Config\ImportStorageTransformer;
use Drupal\Core\Config\MemoryStorage;
use Drupal\Core\Config\StorageTransformerException;
use Drupal\Core\Lock\NullLockBackend;
use Drupal\KernelTests\KernelTestBase;
// cspell:ignore arrr
/**
* Tests the import storage transformer.
*
* @group config
*/
class ImportStorageTransformerTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'config_transformer_test',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(['system']);
}
/**
* Tests the import transformation.
*/
public function testTransform(): void {
// Get the raw system.site config and set it in the sync storage.
$rawConfig = $this->config('system.site')->getRawData();
$storage = new MemoryStorage();
$this->copyConfig($this->container->get('config.storage'), $storage);
$import = $this->container->get('config.import_transformer')->transform($storage);
$config = $import->read('system.site');
// The test subscriber always adds "Arrr" to the current site name.
$this->assertEquals($rawConfig['name'] . ' Arrr', $config['name']);
$this->assertEquals($rawConfig['slogan'], $config['slogan']);
// Update the site config in the storage to test a second transformation.
$config['name'] = 'New name';
$config['slogan'] = 'New slogan';
$storage->write('system.site', $config);
$import = $this->container->get('config.import_transformer')->transform($storage);
$config = $import->read('system.site');
// The test subscriber always adds "Arrr" to the current site name.
$this->assertEquals($rawConfig['name'] . ' Arrr', $config['name']);
$this->assertEquals('New slogan', $config['slogan']);
}
/**
* Tests that the import transformer throws an exception.
*/
public function testTransformLocked(): void {
// Mock the request lock not being available.
$lock = $this->createMock('Drupal\Core\Lock\LockBackendInterface');
$lock->expects($this->exactly(2))
->method('acquire')
->with(ImportStorageTransformer::LOCK_NAME)
->willReturn(FALSE);
$lock->expects($this->once())
->method('wait')
->with(ImportStorageTransformer::LOCK_NAME);
// The import transformer under test.
$transformer = new ImportStorageTransformer(
$this->container->get('event_dispatcher'),
$this->container->get('database'),
$lock,
new NullLockBackend()
);
$this->expectException(StorageTransformerException::class);
$this->expectExceptionMessage("Cannot acquire config import transformer lock.");
$transformer->transform(new MemoryStorage());
}
/**
* Tests the import transformer during a running config import.
*/
public function testTransformWhileImporting(): void {
// Set up the database table with the current active config.
// This simulates the config importer having its transformation done.
$storage = $this->container->get('config.import_transformer')->transform($this->container->get('config.storage'));
// Mock the persistent lock being unavailable due to a config import.
$lock = $this->createMock('Drupal\Core\Lock\LockBackendInterface');
$lock->expects($this->once())
->method('lockMayBeAvailable')
->with(ConfigImporter::LOCK_NAME)
->willReturn(FALSE);
// The import transformer under test.
$transformer = new ImportStorageTransformer(
$this->container->get('event_dispatcher'),
$this->container->get('database'),
new NullLockBackend(),
$lock
);
// Transform an empty memory storage.
$import = $transformer->transform(new MemoryStorage());
// Assert that the transformed storage is the same as the one used to
// set up the simulated config importer.
$this->assertEquals($storage->listAll(), $import->listAll());
$this->assertNotEmpty($import->read('system.site'));
}
}

View File

@ -0,0 +1,394 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests validation of mailer dsn config.
*
* @group config
* @group Validation
*/
class MailerDsnConfigValidationTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system'];
/**
* Config manager service.
*/
protected TypedConfigManagerInterface $configManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig('system');
$this->configManager = $this->container->get(TypedConfigManagerInterface::class);
}
/**
* Tests the validation of the mailer scheme.
*/
public function testMailerSchemeValidation(): void {
$config = $this->config('system.mail');
$this->assertFalse($config->isNew());
$data = $config->get();
// If the scheme is NULL, it should be an error.
$data['mailer_dsn']['scheme'] = NULL;
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(1, $violations);
$this->assertSame('mailer_dsn.scheme', $violations[0]->getPropertyPath());
$this->assertSame('This value should not be null.', (string) $violations[0]->getMessage());
// If the scheme is blank, it should be an error.
$data['mailer_dsn']['scheme'] = '';
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(1, $violations);
$this->assertSame('mailer_dsn.scheme', $violations[0]->getPropertyPath());
$this->assertSame('The mailer DSN must contain a scheme.', (string) $violations[0]->getMessage());
// If the scheme doesn't start with a letter, it should be an error.
$data['mailer_dsn']['scheme'] = '-unexpected-first-character';
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(1, $violations);
$this->assertSame('mailer_dsn.scheme', $violations[0]->getPropertyPath());
$this->assertSame('The mailer DSN scheme must start with a letter followed by zero or more letters, numbers, plus (+), minus (-) or periods (.)', (string) $violations[0]->getMessage());
// If the scheme contains unexpected characters, it should be an error.
$data['mailer_dsn']['scheme'] = 'unexpected_underscore';
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(1, $violations);
$this->assertSame('mailer_dsn.scheme', $violations[0]->getPropertyPath());
$this->assertSame('The mailer DSN scheme must start with a letter followed by zero or more letters, numbers, plus (+), minus (-) or periods (.)', (string) $violations[0]->getMessage());
// If the scheme is valid, it should be accepted.
$data['mailer_dsn']['scheme'] = 'smtp';
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(0, $violations);
// If the scheme is valid, it should be accepted.
$data['mailer_dsn']['scheme'] = 'sendmail+smtp';
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(0, $violations);
// If the scheme is valid, it should be accepted.
$data['mailer_dsn']['scheme'] = 'drupal.test-capture';
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(0, $violations);
}
/**
* Tests the validation of the mailer host.
*/
public function testMailerHostValidation(): void {
$config = $this->config('system.mail');
$this->assertFalse($config->isNew());
$data = $config->get();
// If the host is NULL, it should be an error.
$data['mailer_dsn']['host'] = NULL;
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(1, $violations);
$this->assertSame('mailer_dsn.host', $violations[0]->getPropertyPath());
$this->assertSame('This value should not be null.', (string) $violations[0]->getMessage());
// If the host is blank, it should be an error.
$data['mailer_dsn']['host'] = '';
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(1, $violations);
$this->assertSame('mailer_dsn.host', $violations[0]->getPropertyPath());
$this->assertSame('The mailer DSN must contain a host (use "default" by default).', (string) $violations[0]->getMessage());
// If the host contains a newline, it should be an error.
$data['mailer_dsn']['host'] = "host\nwith\nnewline";
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(1, $violations);
$this->assertSame('mailer_dsn.host', $violations[0]->getPropertyPath());
$this->assertSame('The mailer DSN host should conform to RFC 3986 URI host component.', (string) $violations[0]->getMessage());
// If the host contains unexpected characters, it should be an error.
$data['mailer_dsn']['host'] = "host\rwith\tcontrol-chars";
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(1, $violations);
$this->assertSame('mailer_dsn.host', $violations[0]->getPropertyPath());
$this->assertSame('The mailer DSN host should conform to RFC 3986 URI host component.', (string) $violations[0]->getMessage());
// If the host is valid, it should be accepted.
$data['mailer_dsn']['host'] = 'default';
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(0, $violations);
// If the host is valid, it should be accepted.
$data['mailer_dsn']['host'] = 'mail.example.com';
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(0, $violations);
// If the host is valid, it should be accepted.
$data['mailer_dsn']['host'] = '127.0.0.1';
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(0, $violations);
// If the host is valid, it should be accepted.
$data['mailer_dsn']['host'] = '[::1]';
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(0, $violations);
}
/**
* Tests the validation of the password for the mailer user.
*/
public function testMailerUserPasswordValidation(): void {
$config = $this->config('system.mail');
$this->assertFalse($config->isNew());
$data = $config->get();
// If the user is valid, it should be accepted.
$data['mailer_dsn']['user'] = "any😎thing\ngoes";
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(0, $violations);
// If the password is valid, it should be accepted.
$data['mailer_dsn']['password'] = "any😎thing\ngoes";
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(0, $violations);
}
/**
* Tests the validation of the port used by the mailer.
*/
public function testMailerPortValidation(): void {
$config = $this->config('system.mail');
$this->assertFalse($config->isNew());
$data = $config->get();
// If the port is negative, it should be an error.
$data['mailer_dsn']['port'] = -1;
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(1, $violations);
$this->assertSame('mailer_dsn.port', $violations[0]->getPropertyPath());
$this->assertSame('The mailer DSN port must be between 0 and 65535.', (string) $violations[0]->getMessage());
// If the port greater than 65535, it should be an error.
$data['mailer_dsn']['port'] = 655351 + 1;
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(1, $violations);
$this->assertSame('mailer_dsn.port', $violations[0]->getPropertyPath());
$this->assertSame('The mailer DSN port must be between 0 and 65535.', (string) $violations[0]->getMessage());
// If the port is valid, it should be accepted.
$data['mailer_dsn']['port'] = 587;
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(0, $violations);
}
/**
* Tests the validation of the default options of the mailer.
*/
public function testMailerTransportDefaultOptionsValidation(): void {
$config = $this->config('system.mail');
$this->assertFalse($config->isNew());
$data = $config->get();
// Set scheme to an unknown schema.
$data['mailer_dsn']['scheme'] = 'drupal.unknown-scheme+https';
// If there is no more specific type for a scheme, options with any key
// should be accepted.
$data['mailer_dsn']['options'] = [
'any_bool' => TRUE,
'any_int' => 42,
'any_string' => "any😎thing\ngoes",
];
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(0, $violations);
}
/**
* Tests the validation of the options for the 'native' mailer scheme.
*/
public function testMailerTransportNativeOptionsValidation(): void {
$config = $this->config('system.mail');
$this->assertFalse($config->isNew());
$data = $config->get();
// Set scheme to native.
$data['mailer_dsn']['scheme'] = 'native';
// If the options contain an invalid key, it should be an error.
$data['mailer_dsn']['options'] = ['invalid_key' => 'Hello'];
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(1, $violations);
$this->assertSame('mailer_dsn.options.invalid_key', $violations[0]->getPropertyPath());
$this->assertSame("'invalid_key' is not a supported key.", (string) $violations[0]->getMessage());
// If options is an empty map, it should be accepted.
$data['mailer_dsn']['options'] = [];
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(0, $violations);
}
/**
* Tests the validation of the options for the 'null' mailer scheme.
*/
public function testMailerTransportNullOptionsValidation(): void {
$config = $this->config('system.mail');
$this->assertFalse($config->isNew());
$data = $config->get();
// Set scheme to null.
$data['mailer_dsn']['scheme'] = 'null';
// If the options contain an invalid key, it should be an error.
$data['mailer_dsn']['options'] = ['invalid_key' => 'Hello'];
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(1, $violations);
$this->assertSame('mailer_dsn.options.invalid_key', $violations[0]->getPropertyPath());
$this->assertSame("'invalid_key' is not a supported key.", (string) $violations[0]->getMessage());
// If options is an empty map, it should be accepted.
$data['mailer_dsn']['options'] = [];
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(0, $violations);
}
/**
* Tests the validation of the options for the 'sendmail' mailer scheme.
*/
public function testMailerTransportSendmailOptionsValidation(): void {
$config = $this->config('system.mail');
$this->assertFalse($config->isNew());
$data = $config->get();
// Set scheme to sendmail.
$data['mailer_dsn']['scheme'] = 'sendmail';
// If the options contain an invalid command, it should be an error.
$data['mailer_dsn']['options'] = ['command' => "sendmail\t-bs\n"];
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(1, $violations);
$this->assertSame('mailer_dsn.options.command', $violations[0]->getPropertyPath());
$this->assertSame('The command option is not allowed to span multiple lines or contain control characters.', (string) $violations[0]->getMessage());
// If the options contain an invalid key, it should be an error.
$data['mailer_dsn']['options'] = ['invalid_key' => 'Hello'];
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(1, $violations);
$this->assertSame('mailer_dsn.options.invalid_key', $violations[0]->getPropertyPath());
$this->assertSame("'invalid_key' is not a supported key.", (string) $violations[0]->getMessage());
// If the options contain a command, it should accepted.
$data['mailer_dsn']['options'] = ['command' => 'sendmail -bs'];
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(0, $violations);
// If options is an empty map, it should be accepted.
$data['mailer_dsn']['options'] = [];
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(0, $violations);
}
/**
* Tests the validation of the options for the 'smtps' mailer scheme.
*/
public function testMailerTransportSMTPOptionsValidation(): void {
$config = $this->config('system.mail');
$this->assertFalse($config->isNew());
$data = $config->get();
// Set scheme to smtps.
$data['mailer_dsn']['scheme'] = 'smtps';
// If the options contain an invalid peer_fingerprint, it should be an
// error.
$data['mailer_dsn']['options'] = [
'verify_peer' => FALSE,
'peer_fingerprint' => 'BE:F7:B9:CA:0F:6E:0F:29:9B:E9:B4:64:99:35:D6:27',
];
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(1, $violations);
$this->assertSame('mailer_dsn.options.peer_fingerprint', $violations[0]->getPropertyPath());
$this->assertSame('The peer_fingerprint option requires an md5, sha1 or sha256 certificate fingerprint in hex with all separators (colons) removed.', (string) $violations[0]->getMessage());
// If the options contain a valid peer_fingerprint, it should be accepted.
$data['mailer_dsn']['options'] = [
'verify_peer' => FALSE,
'peer_fingerprint' => 'BEF7B9CA0F6E0F299BE9B4649935D627',
];
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(0, $violations);
// If the options contain a valid peer_fingerprint, it should be accepted.
$data['mailer_dsn']['options'] = [
'verify_peer' => TRUE,
'peer_fingerprint' => '87abbc4d1c3f23146362c6a1168fb7e90a56569c4c97275c69c0630dc06e526d',
];
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(0, $violations);
// If the options contain a local_domain with a newline, it should be an
// error.
$data['mailer_dsn']['options'] = ['local_domain' => "host\nwith\nnewline"];
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(1, $violations);
$this->assertSame('mailer_dsn.options.local_domain', $violations[0]->getPropertyPath());
$this->assertSame('The local_domain is not allowed to span multiple lines or contain control characters.', (string) $violations[0]->getMessage());
// If the options contain a local_domain with unexpected characters, it
// should be an error.
$data['mailer_dsn']['options'] = ['local_domain' => "host\rwith\tcontrol-chars"];
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(1, $violations);
$this->assertSame('mailer_dsn.options.local_domain', $violations[0]->getPropertyPath());
$this->assertSame('The local_domain is not allowed to span multiple lines or contain control characters.', (string) $violations[0]->getMessage());
// If the options contain a valid local_domain, it should be accepted.
$data['mailer_dsn']['options'] = ['local_domain' => 'www.example.com'];
$violations = $this->configManager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(0, $violations);
}
}

View File

@ -0,0 +1,209 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\Core\Config\Schema\SchemaCheckTrait;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the functionality of SchemaCheckTrait.
*
* @group config
*/
class SchemaCheckTraitTest extends KernelTestBase {
use SchemaCheckTrait;
/**
* The typed config manager.
*
* @var \Drupal\Core\Config\TypedConfigManagerInterface
*/
protected $typedConfig;
/**
* {@inheritdoc}
*/
protected static $modules = ['config_test', 'config_schema_test'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(['config_test', 'config_schema_test']);
$this->typedConfig = \Drupal::service('config.typed');
}
/**
* Tests \Drupal\Core\Config\Schema\SchemaCheckTrait.
*
* @dataProvider providerCheckConfigSchema
*/
public function testCheckConfigSchema(string $type_to_validate_against, bool $validate_constraints, array|bool $nulled_expectations, array|bool $no_data_expectations, array $expectations): void {
// Test a non existing schema.
$ret = $this->checkConfigSchema($this->typedConfig, 'config_schema_test.no_schema', $this->config('config_schema_test.no_schema')->get());
$this->assertFalse($ret);
// Test an existing schema with valid data.
$config_data = $this->config('config_test.types')->get();
$ret = $this->checkConfigSchema($this->typedConfig, 'config_test.types', $config_data);
$this->assertTrue($ret);
// Test it is possible to mark any schema type as required (not nullable).
$nulled_config_data = array_fill_keys(array_keys($config_data), NULL);
$ret = $this->checkConfigSchema($this->typedConfig, $type_to_validate_against, $nulled_config_data, $validate_constraints);
$this->assertSame($nulled_expectations, $ret);
// Add a new key, a new array and overwrite boolean with array to test the
// error messages.
$config_data = ['new_key' => 'new_value', 'new_array' => []] + $config_data;
$config_data['boolean'] = [];
$ret = $this->checkConfigSchema($this->typedConfig, $type_to_validate_against, $config_data, $validate_constraints);
$this->assertEquals($expectations, $ret);
// Omit all data, this should trigger validation errors for required keys
// missing.
$config_data = [];
$ret = $this->checkConfigSchema($this->typedConfig, $type_to_validate_against, $config_data, $validate_constraints);
$this->assertEquals($no_data_expectations, $ret);
}
/**
* Returns test data for validating configuration schema.
*/
public static function providerCheckConfigSchema(): array {
// Storage type check errors.
// @see \Drupal\Core\Config\Schema\SchemaCheckTrait::checkValue()
$expected_storage_null_check_errors = [
// TRICKY: `_core` is added during installation even if it is absent from
// core/modules/config/tests/config_test/config/install/config_test.dynamic.dotted.default.yml.
// @see \Drupal\Core\Config\ConfigInstaller::createConfiguration()
'config_test.types:_core' => 'variable type is NULL but applied schema class is Drupal\Core\Config\Schema\Mapping',
'config_test.types:array' => 'variable type is NULL but applied schema class is Drupal\Core\Config\Schema\Sequence',
'config_test.types:mapping_with_only_required_keys' => 'variable type is NULL but applied schema class is Drupal\Core\Config\Schema\Mapping',
'config_test.types:mapping_with_some_required_keys' => 'variable type is NULL but applied schema class is Drupal\Core\Config\Schema\Mapping',
'config_test.types:mapping_with_only_optional_keys' => 'variable type is NULL but applied schema class is Drupal\Core\Config\Schema\Mapping',
];
$expected_storage_type_check_errors = [
'config_test.types:new_key' => 'missing schema',
'config_test.types:new_array' => 'missing schema',
'config_test.types:boolean' => 'non-scalar value but not defined as an array (such as mapping or sequence)',
];
// Validation constraints violations.
// @see \Drupal\Core\TypedData\TypedDataInterface::validate()
$expected_validation_errors = [
'0' => "[new_key] 'new_key' is not a supported key.",
'1' => "[new_array] 'new_array' is not a supported key.",
'2' => '[boolean] This value should be of the correct primitive type.',
];
$basic_cases = [
'config_test.types, without validation' => [
'config_test.types',
FALSE,
$expected_storage_null_check_errors,
TRUE,
$expected_storage_type_check_errors,
],
'config_test.types, with validation' => [
'config_test.types',
TRUE,
$expected_storage_null_check_errors,
TRUE,
$expected_storage_type_check_errors + $expected_validation_errors,
],
];
// Test that if the exact same schema is reused but now has the constraint
// "FullyValidatable" specified at the top level, that:
// 1. `NULL` values now trigger validation errors, except when
// `nullable: true` is set.
// 2. missing required keys now trigger validation errors, except when
// `requiredKey: false` is set.
// @see `type: config_test.types.fully_validatable`
// @see core/modules/config/tests/config_test/config/schema/config_test.schema.yml
$expected_storage_null_check_errors = [
// TRICKY: `_core` is added during installation even if it is absent from
// core/modules/config/tests/config_test/config/install/config_test.dynamic.dotted.default.yml.
// @see \Drupal\Core\Config\ConfigInstaller::createConfiguration()
'config_test.types.fully_validatable:_core' => 'variable type is NULL but applied schema class is Drupal\Core\Config\Schema\Mapping',
'config_test.types.fully_validatable:array' => 'variable type is NULL but applied schema class is Drupal\Core\Config\Schema\Sequence',
'config_test.types.fully_validatable:mapping_with_only_required_keys' => 'variable type is NULL but applied schema class is Drupal\Core\Config\Schema\Mapping',
'config_test.types.fully_validatable:mapping_with_some_required_keys' => 'variable type is NULL but applied schema class is Drupal\Core\Config\Schema\Mapping',
'config_test.types.fully_validatable:mapping_with_only_optional_keys' => 'variable type is NULL but applied schema class is Drupal\Core\Config\Schema\Mapping',
];
$expected_storage_type_check_errors = [
'config_test.types.fully_validatable:new_key' => 'missing schema',
'config_test.types.fully_validatable:new_array' => 'missing schema',
'config_test.types.fully_validatable:boolean' => 'non-scalar value but not defined as an array (such as mapping or sequence)',
];
$opt_in_cases = [
'config_test.types.fully_validatable, without validation' => [
'config_test.types.fully_validatable',
FALSE,
$expected_storage_null_check_errors,
TRUE,
$expected_storage_type_check_errors,
],
'config_test.types.fully_validatable, with validation' => [
'config_test.types.fully_validatable',
TRUE,
$expected_storage_null_check_errors + [
'[_core] This value should not be null.',
'[array] This value should not be null.',
'[boolean] This value should not be null.',
'[exp] This value should not be null.',
'[float] This value should not be null.',
'[float_as_integer] This value should not be null.',
'[hex] This value should not be null.',
'[int] This value should not be null.',
'[string] This value should not be null.',
'[string_int] This value should not be null.',
'[mapping_with_only_required_keys] This value should not be null.',
'[mapping_with_some_required_keys] This value should not be null.',
'[mapping_with_only_optional_keys] This value should not be null.',
],
[
"[] 'array' is a required key.",
"[] 'boolean' is a required key.",
"[] 'exp' is a required key.",
"[] 'float' is a required key.",
"[] 'float_as_integer' is a required key.",
"[] 'hex' is a required key.",
"[] 'int' is a required key.",
"[] 'string' is a required key.",
"[] 'string_int' is a required key.",
"[] 'nullable_array' is a required key.",
"[] 'nullable_boolean' is a required key.",
"[] 'nullable_exp' is a required key.",
"[] 'nullable_float' is a required key.",
"[] 'nullable_float_as_integer' is a required key.",
"[] 'nullable_hex' is a required key.",
"[] 'nullable_int' is a required key.",
"[] 'nullable_octal' is a required key.",
"[] 'nullable_string' is a required key.",
"[] 'nullable_string_int' is a required key.",
"[] 'mapping_with_only_required_keys' is a required key.",
"[] 'mapping_with_some_required_keys' is a required key.",
"[] 'mapping_with_only_optional_keys' is a required key.",
],
$expected_storage_type_check_errors + $expected_validation_errors + [
// For `mapping_with_only_required_keys`: errors for all 4 keys.
3 => "[mapping_with_only_required_keys] 'north' is a required key.",
4 => "[mapping_with_only_required_keys] 'east' is a required key.",
5 => "[mapping_with_only_required_keys] 'south' is a required key.",
6 => "[mapping_with_only_required_keys] 'west' is a required key.",
// For `mapping_with_some_required_keys`: errors for 2 required keys.
7 => "[mapping_with_some_required_keys] 'north' is a required key.",
8 => "[mapping_with_some_required_keys] 'south' is a required key.",
],
],
];
return array_merge($basic_cases, $opt_in_cases);
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\Traits\Core\Config\SchemaConfigListenerTestTrait;
/**
* Tests the functionality of ConfigSchemaChecker in KernelTestBase tests.
*
* @group config
*/
class SchemaConfigListenerTest extends KernelTestBase {
use SchemaConfigListenerTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['config_test'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Install configuration provided by the module so that the order of the
// config keys is the same as
// \Drupal\FunctionalTests\Core\Config\SchemaConfigListenerTest.
$this->installConfig(['config_test']);
}
}

View File

@ -0,0 +1,179 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests validation of certain elements common to all config.
*
* @group config
* @group Validation
*/
class SimpleConfigValidationTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig('system');
}
/**
* Tests the validation of the default configuration hash.
*/
public function testDefaultConfigHashValidation(): void {
$config = $this->config('system.site');
$this->assertFalse($config->isNew());
$data = $config->get();
$original_hash = $data['_core']['default_config_hash'];
$this->assertNotEmpty($original_hash);
/** @var \Drupal\Core\Config\TypedConfigManagerInterface $typed_config_manager */
$typed_config_manager = $this->container->get('config.typed');
// If the default_config_hash is NULL, it should be an error.
$data['_core']['default_config_hash'] = NULL;
$violations = $typed_config_manager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(1, $violations);
$this->assertSame('_core.default_config_hash', $violations[0]->getPropertyPath());
$this->assertSame('This value should not be null.', (string) $violations[0]->getMessage());
// Config hashes must be 43 characters long.
$data['_core']['default_config_hash'] = $original_hash . '-long';
$violations = $typed_config_manager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(1, $violations);
$this->assertSame('_core.default_config_hash', $violations[0]->getPropertyPath());
$this->assertSame('This value should have exactly <em class="placeholder">43</em> characters.', (string) $violations[0]->getMessage());
// Config hashes can only contain certain characters, and spaces aren't one
// of them. If we replace the final character of the original hash with a
// space, we should get an error.
$data['_core']['default_config_hash'] = substr($original_hash, 0, -1) . ' ';
$violations = $typed_config_manager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(1, $violations);
$this->assertSame('_core.default_config_hash', $violations[0]->getPropertyPath());
$this->assertSame('This value is not valid.', (string) $violations[0]->getMessage());
$data['_core']['default_config_hash'] = $original_hash;
$data['_core']['invalid_key'] = 'Hello';
$violations = $typed_config_manager->createFromNameAndData($config->getName(), $data)
->validate();
$this->assertCount(1, $violations);
$this->assertSame('_core.invalid_key', $violations[0]->getPropertyPath());
$this->assertSame("'invalid_key' is not a supported key.", (string) $violations[0]->getMessage());
}
/**
* Data provider for ::testSpecialCharacters().
*
* @return array[]
* The test cases.
*/
public static function providerSpecialCharacters(): array {
$data = [];
for ($code_point = 0; $code_point < 32; $code_point++) {
$data["label $code_point"] = [
'system.site',
'name',
mb_chr($code_point),
'Labels are not allowed to span multiple lines or contain control characters.',
];
$data["text $code_point"] = [
'system.maintenance',
'message',
mb_chr($code_point),
'Text is not allowed to contain control characters, only visible characters.',
];
}
// Line feeds (ASCII 10) and carriage returns (ASCII 13) are used to create
// new lines, so they are allowed in text data, along with tabs (ASCII 9).
$data['text 9'][3] = $data['text 10'][3] = $data['text 13'][3] = NULL;
// Ensure emoji are allowed.
$data['emoji in label'] = [
'system.site',
'name',
'😎',
NULL,
];
$data['emoji in text'] = [
'system.maintenance',
'message',
'🤓',
NULL,
];
return $data;
}
/**
* Tests that special characters are not allowed in labels or text data.
*
* @param string $config_name
* The name of the simple config to test with.
* @param string $property
* The config property in which to embed a control character.
* @param string $character
* A special character to embed.
* @param string|null $expected_error_message
* The expected validation error message, if any.
*
* @dataProvider providerSpecialCharacters
*/
public function testSpecialCharacters(string $config_name, string $property, string $character, ?string $expected_error_message): void {
$config = $this->config($config_name)
->set($property, "This has a special character: $character");
$violations = $this->container->get('config.typed')
->createFromNameAndData($config->getName(), $config->get())
->validate();
if ($expected_error_message === NULL) {
$this->assertCount(0, $violations);
}
else {
$code_point = mb_ord($character);
$this->assertCount(1, $violations, "Character $code_point did not raise a constraint violation.");
$this->assertSame($property, $violations[0]->getPropertyPath());
$this->assertSame($expected_error_message, (string) $violations[0]->getMessage());
}
}
/**
* Tests that plugin IDs in simple config are validated.
*
* @param string $config_name
* The name of the config object to validate.
* @param string $property
* The property path to set. This will receive the value 'non_existent' and
* is expected to raise a "plugin does not exist" error.
*
* @testWith ["system.mail", "interface.0"]
* ["system.image", "toolkit"]
*/
public function testInvalidPluginId(string $config_name, string $property): void {
$config = $this->config($config_name);
$violations = $this->container->get('config.typed')
->createFromNameAndData($config_name, $config->set($property, 'non_existent')->get())
->validate();
$this->assertCount(1, $violations);
$this->assertSame($property, $violations[0]->getPropertyPath());
$this->assertSame("The 'non_existent' plugin does not exist.", (string) $violations[0]->getMessage());
}
}

View File

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config\Storage;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Config\CachedStorage;
use Drupal\Core\StreamWrapper\PublicStream;
/**
* Tests CachedStorage operations.
*
* @group config
*/
class CachedStorageTest extends ConfigStorageTestBase {
/**
* The cache backend the cached storage is using.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;
/**
* The file storage the cached storage is using.
*
* @var \Drupal\Core\Config\FileStorage
*/
protected $fileStorage;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a directory.
$dir = PublicStream::basePath() . '/config';
$this->fileStorage = new FileStorage($dir);
$this->storage = new CachedStorage($this->fileStorage, \Drupal::service('cache.config'));
$this->cache = \Drupal::service('cache_factory')->get('config');
}
/**
* {@inheritdoc}
*/
public function testInvalidStorage(): void {
$this->markTestSkipped('No-op as this test does not make sense');
}
/**
* {@inheritdoc}
*/
protected function read($name) {
$data = $this->cache->get($name);
// Cache misses fall through to the underlying storage.
return $data ? $data->data : $this->fileStorage->read($name);
}
/**
* {@inheritdoc}
*/
protected function insert($name, $data): void {
$this->fileStorage->write($name, $data);
$this->cache->set($name, $data);
}
/**
* {@inheritdoc}
*/
protected function update($name, $data): void {
$this->fileStorage->write($name, $data);
$this->cache->set($name, $data);
}
/**
* {@inheritdoc}
*/
protected function delete($name): void {
$this->cache->delete($name);
unlink($this->fileStorage->getFilePath($name));
}
}

View File

@ -0,0 +1,331 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config\Storage\Checkpoint;
use Drupal\Core\Config\Checkpoint\CheckpointStorageInterface;
use Drupal\Core\Config\ConfigImporter;
use Drupal\Core\Config\StorageComparer;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests CheckpointStorage operations.
*
* @group config
*/
class CheckpointStorageTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system', 'config_test'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(['system', 'config_test']);
}
/**
* Tests the save and read operations of checkpoint storage.
*/
public function testConfigSaveAndRead(): void {
$checkpoint_storage = $this->container->get('config.storage.checkpoint');
$this->config('system.site')->set('name', 'Test1')->save();
$check1 = $checkpoint_storage->checkpoint('A');
$this->config('system.site')->set('name', 'Test2')->save();
$check2 = $checkpoint_storage->checkpoint('B');
$this->config('system.site')->set('name', 'Test3')->save();
$this->assertSame('Test3', $this->config('system.site')->get('name'));
$this->assertSame('Test1', $checkpoint_storage->read('system.site')['name']);
// The config listings should be exactly the same.
$this->assertSame($checkpoint_storage->listAll(), $this->container->get('config.storage')->listAll());
$checkpoint_storage->setCheckpointToReadFrom($check2);
$this->assertSame('Test2', $checkpoint_storage->read('system.site')['name']);
$this->assertSame($checkpoint_storage->listAll(), $this->container->get('config.storage')->listAll());
$checkpoint_storage->setCheckpointToReadFrom($check1);
$this->assertSame('Test1', $checkpoint_storage->read('system.site')['name']);
$this->assertSame($checkpoint_storage->listAll(), $this->container->get('config.storage')->listAll());
}
/**
* Tests the delete operation of checkpoint storage.
*/
public function testConfigDelete(): void {
$checkpoint_storage = $this->container->get('config.storage.checkpoint');
$check1 = $checkpoint_storage->checkpoint('A');
$this->config('config_test.system')->delete();
$this->assertFalse($this->container->get('config.storage')->exists('config_test.system'));
$this->assertTrue($checkpoint_storage->exists('config_test.system'));
$this->assertSame('bar', $checkpoint_storage->read('config_test.system')['foo']);
$this->assertContains('config_test.system', $checkpoint_storage->listAll());
$this->assertContains('config_test.system', $checkpoint_storage->listAll('config_test.'));
$this->assertNotContains('config_test.system', $checkpoint_storage->listAll('system.'));
// Should not be part of the active storage anymore.
$this->assertNotContains('config_test.system', $this->container->get('config.storage')->listAll());
$check2 = $checkpoint_storage->checkpoint('B');
$this->config('config_test.system')->set('foo', 'foobar')->save();
$this->assertTrue($this->container->get('config.storage')->exists('config_test.system'));
$this->assertTrue($checkpoint_storage->exists('config_test.system'));
$this->assertSame('bar', $checkpoint_storage->read('config_test.system')['foo']);
$checkpoint_storage->setCheckpointToReadFrom($check2);
$this->assertFalse($checkpoint_storage->exists('config_test.system'));
$this->assertFalse($checkpoint_storage->read('config_test.system'));
$this->assertNotContains('config_test.system', $checkpoint_storage->listAll());
$checkpoint_storage->setCheckpointToReadFrom($check1);
$this->assertTrue($checkpoint_storage->exists('config_test.system'));
$this->assertSame('bar', $checkpoint_storage->read('config_test.system')['foo']);
$this->assertContains('config_test.system', $checkpoint_storage->listAll());
}
/**
* Tests the create operation of checkpoint storage.
*/
public function testConfigCreate(): void {
$checkpoint_storage = $this->container->get('config.storage.checkpoint');
$this->config('config_test.system')->delete();
$check1 = $checkpoint_storage->checkpoint('A');
$this->config('config_test.system')->set('foo', 'foobar')->save();
$this->assertTrue($this->container->get('config.storage')->exists('config_test.system'));
$this->assertFalse($checkpoint_storage->exists('config_test.system'));
$this->assertFalse($checkpoint_storage->read('config_test.system'));
$this->assertNotContains('config_test.system', $checkpoint_storage->listAll());
$this->assertNotContains('config_test.system', $checkpoint_storage->listAll('config_test.'));
$this->assertContains('system.site', $checkpoint_storage->listAll('system.'));
$this->assertContains('config_test.system', $this->container->get('config.storage')->listAll());
$check2 = $checkpoint_storage->checkpoint('B');
$this->config('config_test.system')->delete();
$this->assertFalse($this->container->get('config.storage')->exists('config_test.system'));
$this->assertFalse($checkpoint_storage->exists('config_test.system'));
$this->assertFalse($checkpoint_storage->read('config_test.system'));
$this->config('config_test.system')->set('foo', 'foobar')->save();
$this->assertTrue($this->container->get('config.storage')->exists('config_test.system'));
$this->assertFalse($checkpoint_storage->exists('config_test.system'));
$this->assertFalse($checkpoint_storage->read('config_test.system'));
$checkpoint_storage->setCheckpointToReadFrom($check2);
$this->assertTrue($checkpoint_storage->exists('config_test.system'));
$this->assertSame('foobar', $checkpoint_storage->read('config_test.system')['foo']);
$this->assertContains('config_test.system', $checkpoint_storage->listAll());
$checkpoint_storage->setCheckpointToReadFrom($check1);
$this->assertFalse($checkpoint_storage->exists('config_test.system'));
$this->assertFalse($checkpoint_storage->read('config_test.system'));
$this->assertNotContains('config_test.system', $checkpoint_storage->listAll());
}
/**
* Tests the rename operation of checkpoint storage.
*/
public function testConfigRename(): void {
$checkpoint_storage = $this->container->get('config.storage.checkpoint');
$check1 = $checkpoint_storage->checkpoint('A');
$this->container->get('config.factory')->rename('config_test.dynamic.dotted.default', 'config_test.dynamic.renamed');
$this->config('config_test.dynamic.renamed')->set('id', 'renamed')->save();
$this->assertFalse($checkpoint_storage->exists('config_test.dynamic.renamed'));
$this->assertTrue($checkpoint_storage->exists('config_test.dynamic.dotted.default'));
$this->assertSame('dotted.default', $checkpoint_storage->read('config_test.dynamic.dotted.default')['id']);
$this->assertSame($checkpoint_storage->read('config_test.dynamic.dotted.default')['uuid'], $this->config('config_test.dynamic.renamed')->get('uuid'));
$check2 = $checkpoint_storage->checkpoint('B');
/** @var \Drupal\Core\Config\Entity\ConfigEntityStorage $storage */
$storage = $this->container->get('entity_type.manager')->getStorage('config_test');
// Entity1 will be deleted by the test.
$entity1 = $storage->create(
[
'id' => 'dotted.default',
'label' => 'Another one',
]
);
$entity1->save();
$check3 = $checkpoint_storage->checkpoint('C');
$checkpoint_storage->setCheckpointToReadFrom($check2);
$this->assertFalse($checkpoint_storage->exists('config_test.dynamic.dotted.default'));
$checkpoint_storage->setCheckpointToReadFrom($check3);
$this->assertTrue($checkpoint_storage->exists('config_test.dynamic.dotted.default'));
$this->assertNotEquals($checkpoint_storage->read('config_test.dynamic.dotted.default')['uuid'], $this->config('config_test.dynamic.renamed')->get('uuid'));
$this->assertSame('Another one', $checkpoint_storage->read('config_test.dynamic.dotted.default')['label']);
$checkpoint_storage->setCheckpointToReadFrom($check1);
$this->assertSame('Default', $checkpoint_storage->read('config_test.dynamic.dotted.default')['label']);
}
/**
* Tests the revert operation of checkpoint storage.
*/
public function testRevert(): void {
$checkpoint_storage = $this->container->get('config.storage.checkpoint');
$check1 = $checkpoint_storage->checkpoint('A');
$this->assertTrue($this->container->get('module_installer')->uninstall(['config_test']));
$checkpoint_storage = $this->container->get('config.storage.checkpoint');
$check2 = $checkpoint_storage->checkpoint('B');
$importer = $this->getConfigImporter($checkpoint_storage);
$config_changelist = $importer->getStorageComparer()->createChangelist()->getChangelist();
$this->assertContains('config_test.dynamic.dotted.default', $config_changelist['create']);
$this->assertSame(['core.extension'], $config_changelist['update']);
$this->assertSame([], $config_changelist['delete']);
$this->assertSame([], $config_changelist['rename']);
$importer->import();
$this->assertSame([], $importer->getErrors());
$this->assertTrue($this->container->get('module_handler')->moduleExists('config_test'));
$checkpoint_storage = $this->container->get('config.storage.checkpoint');
$checkpoint_storage->setCheckpointToReadFrom($check2);
$importer = $this->getConfigImporter($checkpoint_storage);
$config_changelist = $importer->getStorageComparer()->createChangelist()->getChangelist();
$this->assertContains('config_test.dynamic.dotted.default', $config_changelist['delete']);
$this->assertSame(['core.extension'], $config_changelist['update']);
$this->assertSame([], $config_changelist['create']);
$this->assertSame([], $config_changelist['rename']);
$importer->import();
$this->assertFalse($this->container->get('module_handler')->moduleExists('config_test'));
$checkpoint_storage->setCheckpointToReadFrom($check1);
$importer = $this->getConfigImporter($checkpoint_storage);
$importer->getStorageComparer()->createChangelist();
$importer->import();
$this->assertTrue($this->container->get('module_handler')->moduleExists('config_test'));
}
/**
* Tests the rename operation of checkpoint storage with collections.
*/
public function testRevertWithCollections(): void {
$collections = [
'another_collection',
'collection.test1',
'collection.test2',
];
// Set the event listener to return three possible collections.
// @see \Drupal\config_collection_install_test\EventSubscriber
\Drupal::state()->set('config_collection_install_test.collection_names', $collections);
$checkpoint_storage = $this->container->get('config.storage.checkpoint');
$checkpoint_storage->checkpoint('A');
// Install the test module.
$this->assertTrue($this->container->get('module_installer')->install(['config_collection_install_test']));
$checkpoint_storage = $this->container->get('config.storage.checkpoint');
/** @var \Drupal\Core\Config\StorageInterface $active_storage */
$active_storage = \Drupal::service('config.storage');
$this->assertEquals($collections, $active_storage->getAllCollectionNames());
foreach ($collections as $collection) {
$collection_storage = $active_storage->createCollection($collection);
$data = $collection_storage->read('config_collection_install_test.test');
$this->assertEquals($collection, $data['collection']);
}
$check2 = $checkpoint_storage->checkpoint('B');
$importer = $this->getConfigImporter($checkpoint_storage);
$storage_comparer = $importer->getStorageComparer();
$config_changelist = $storage_comparer->createChangelist()->getChangelist();
$this->assertSame([], $config_changelist['create']);
$this->assertSame(['core.extension'], $config_changelist['update']);
$this->assertSame([], $config_changelist['delete']);
$this->assertSame([], $config_changelist['rename']);
foreach ($collections as $collection) {
$config_changelist = $storage_comparer->getChangelist(NULL, $collection);
$this->assertSame([], $config_changelist['create']);
$this->assertSame([], $config_changelist['update']);
$this->assertSame(['config_collection_install_test.test'], $config_changelist['delete'], $collection);
$this->assertSame([], $config_changelist['rename']);
}
$importer->import();
$this->assertSame([], $importer->getErrors());
$checkpoint_storage = $this->container->get('config.storage.checkpoint');
/** @var \Drupal\Core\Config\StorageInterface $active_storage */
$active_storage = \Drupal::service('config.storage');
$this->assertEmpty($active_storage->getAllCollectionNames());
foreach ($collections as $collection) {
$collection_storage = $active_storage->createCollection($collection);
$this->assertFalse($collection_storage->read('config_collection_install_test.test'));
}
$checkpoint_storage->setCheckpointToReadFrom($check2);
$importer = $this->getConfigImporter($checkpoint_storage);
$storage_comparer = $importer->getStorageComparer();
$config_changelist = $storage_comparer->createChangelist()->getChangelist();
$this->assertSame([], $config_changelist['create']);
$this->assertSame(['core.extension'], $config_changelist['update']);
$this->assertSame([], $config_changelist['delete']);
$this->assertSame([], $config_changelist['rename']);
foreach ($collections as $collection) {
$config_changelist = $storage_comparer->getChangelist(NULL, $collection);
$this->assertSame(['config_collection_install_test.test'], $config_changelist['create']);
$this->assertSame([], $config_changelist['update']);
$this->assertSame([], $config_changelist['delete'], $collection);
$this->assertSame([], $config_changelist['rename']);
}
$importer->import();
$this->assertSame([], $importer->getErrors());
$this->assertTrue($this->container->get('module_handler')->moduleExists('config_collection_install_test'));
/** @var \Drupal\Core\Config\StorageInterface $active_storage */
$active_storage = \Drupal::service('config.storage');
$this->assertEquals($collections, $active_storage->getAllCollectionNames());
foreach ($collections as $collection) {
$collection_storage = $active_storage->createCollection($collection);
$data = $collection_storage->read('config_collection_install_test.test');
$this->assertEquals($collection, $data['collection']);
}
}
/**
* Gets the configuration importer.
*/
private function getConfigImporter(CheckpointStorageInterface $storage): ConfigImporter {
$storage_comparer = new StorageComparer(
$storage,
$this->container->get('config.storage')
);
return new ConfigImporter(
$storage_comparer,
$this->container->get('event_dispatcher'),
$this->container->get('config.manager'),
$this->container->get('lock'),
$this->container->get('config.typed'),
$this->container->get('module_handler'),
$this->container->get('module_installer'),
$this->container->get('theme_handler'),
$this->container->get('string_translation'),
$this->container->get('extension.list.module'),
$this->container->get('extension.list.theme')
);
}
}

View File

@ -0,0 +1,297 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config\Storage;
use Drupal\KernelTests\KernelTestBase;
/**
* Base class for testing storage operations.
*
* All configuration storages are expected to behave identically in
* terms of reading, writing, listing, deleting, as well as error handling.
*
* Therefore, storage tests use an uncommon test case class structure;
* the base class defines the test method(s) to execute, which are identical
* for all storages. The storage specific test case classes supply the
* necessary helper methods to interact with the raw/native storage
* directly.
*/
abstract class ConfigStorageTestBase extends KernelTestBase {
/**
* @var \Drupal\Core\Config\StorageInterface
*/
protected $storage;
/**
* @var \Drupal\Core\Config\StorageInterface
*/
protected $invalidStorage;
/**
* Tests storage CRUD operations.
*
* @todo Coverage: Trigger PDOExceptions / Database exceptions.
*/
public function testCRUD(): void {
$name = 'config_test.storage';
// Checking whether a non-existing name exists returns FALSE.
$this->assertFalse($this->storage->exists($name));
// Checking whether readMultiple() works with empty storage.
$this->assertEmpty($this->storage->readMultiple([$name]));
// readMultiple() accepts an empty array.
$this->assertSame([], $this->storage->readMultiple([]), 'Empty query should return empty array');
// Reading a non-existing name returns FALSE.
$data = $this->storage->read($name);
$this->assertFalse($data);
// Writing data returns TRUE and the data has been written.
$data = ['foo' => 'bar'];
$result = $this->storage->write($name, $data);
$this->assertTrue($result);
$raw_data = $this->read($name);
$this->assertSame($data, $raw_data);
// Checking whether an existing name exists returns TRUE.
$this->assertTrue($this->storage->exists($name));
// Writing the identical data again still returns TRUE.
$result = $this->storage->write($name, $data);
$this->assertTrue($result);
// Listing all names returns all.
$this->storage->write('system.performance', []);
$names = $this->storage->listAll();
$this->assertContains('system.performance', $names);
$this->assertContains($name, $names);
// Listing all names with prefix returns names with that prefix only.
$names = $this->storage->listAll('config_test.');
$this->assertNotContains('system.performance', $names);
$this->assertContains($name, $names);
// Rename the configuration storage object.
$new_name = 'config_test.storage_rename';
$this->storage->rename($name, $new_name);
$raw_data = $this->read($new_name);
$this->assertSame($data, $raw_data);
// Rename it back so further tests work.
$this->storage->rename($new_name, $name);
// Deleting an existing name returns TRUE.
$result = $this->storage->delete($name);
$this->assertTrue($result);
// Deleting a non-existing name returns FALSE.
$result = $this->storage->delete($name);
$this->assertFalse($result);
// Deleting all names with prefix deletes the appropriate data and returns
// TRUE.
$files = [
'config_test.test.biff',
'config_test.test.bang',
'config_test.test.pow',
];
foreach ($files as $name) {
$this->storage->write($name, $data);
}
// Test that deleting a prefix that returns no configuration returns FALSE
// because nothing is deleted.
$this->assertFalse($this->storage->deleteAll('some_thing_that_cannot_exist'));
$result = $this->storage->deleteAll('config_test.');
$names = $this->storage->listAll('config_test.');
$this->assertTrue($result);
$this->assertSame([], $names);
// Test renaming an object that does not exist returns FALSE.
$this->assertFalse($this->storage->rename('config_test.storage_does_not_exist', 'config_test.storage_does_not_exist_rename'));
// Test renaming to an object that already returns FALSE.
$data = ['foo' => 'bar'];
$this->assertTrue($this->storage->write($name, $data));
$this->assertFalse($this->storage->rename('config_test.storage_does_not_exist', $name));
}
/**
* Tests an invalid storage.
*/
public function testInvalidStorage(): void {
$name = 'config_test.storage';
// Write something to the valid storage to prove that the storages do not
// pollute one another.
$data = ['foo' => 'bar'];
$result = $this->storage->write($name, $data);
$this->assertTrue($result);
$raw_data = $this->read($name);
$this->assertSame($data, $raw_data);
// Reading from a non-existing storage bin returns FALSE.
$result = $this->invalidStorage->read($name);
$this->assertFalse($result);
// Deleting from a non-existing storage bin throws an exception.
try {
$this->invalidStorage->delete($name);
$this->fail('Exception not thrown upon deleting from a non-existing storage bin.');
}
catch (\Exception) {
// An exception occurred as expected; just continue.
}
// Listing on a non-existing storage bin returns an empty array.
$result = $this->invalidStorage->listAll();
$this->assertSame([], $result);
// Getting all collections on a non-existing storage bin return an empty
// array.
$this->assertSame([], $this->invalidStorage->getAllCollectionNames());
// Writing to a non-existing storage bin creates the bin.
$this->invalidStorage->write($name, ['foo' => 'bar']);
$result = $this->invalidStorage->read($name);
$this->assertSame(['foo' => 'bar'], $result);
}
/**
* Tests storage writing and reading data preserving data type.
*/
public function testDataTypes(): void {
$name = 'config_test.types';
$data = [
'array' => [],
'boolean' => TRUE,
'exp' => 1.2e+34,
'float' => 3.14159,
'hex' => 0xC,
'int' => 99,
'octal' => 0775,
'string' => 'string',
'string_int' => '1',
];
$result = $this->storage->write($name, $data);
$this->assertTrue($result);
$read_data = $this->storage->read($name);
$this->assertSame($data, $read_data);
}
/**
* Tests that the storage supports collections.
*/
public function testCollection(): void {
$name = 'config_test.storage';
$data = ['foo' => 'bar'];
$result = $this->storage->write($name, $data);
$this->assertTrue($result);
$this->assertSame($data, $this->storage->read($name));
// Create configuration in a new collection.
$new_storage = $this->storage->createCollection('collection.sub.new');
$this->assertFalse($new_storage->exists($name));
$this->assertEquals([], $new_storage->listAll());
$this->assertFalse($new_storage->delete($name));
$this->assertFalse($new_storage->deleteAll('config_test.'));
$this->assertFalse($new_storage->deleteAll());
$this->assertFalse($new_storage->rename($name, 'config_test.another_name'));
$new_storage->write($name, $data);
$this->assertTrue($result);
$this->assertSame($data, $new_storage->read($name));
$this->assertEquals([$name], $new_storage->listAll());
$this->assertTrue($new_storage->exists($name));
$new_data = ['foo' => 'baz'];
$new_storage->write($name, $new_data);
$this->assertTrue($result);
$this->assertSame($new_data, $new_storage->read($name));
// Create configuration in another collection.
$another_storage = $this->storage->createCollection('collection.sub.another');
$this->assertFalse($another_storage->exists($name));
$this->assertEquals([], $another_storage->listAll());
$another_storage->write($name, $new_data);
$this->assertTrue($result);
$this->assertSame($new_data, $another_storage->read($name));
$this->assertEquals([$name], $another_storage->listAll());
$this->assertTrue($another_storage->exists($name));
// Create configuration in yet another collection.
$alt_storage = $this->storage->createCollection('alternate');
$alt_storage->write($name, $new_data);
$this->assertTrue($result);
$this->assertSame($new_data, $alt_storage->read($name));
// Switch back to the collection-less mode and check the data still exists
// add has not been touched.
$this->assertSame($data, $this->storage->read($name));
// Check that the getAllCollectionNames() method works.
$this->assertSame(['alternate', 'collection.sub.another', 'collection.sub.new'], $this->storage->getAllCollectionNames());
// Check that the collections are removed when they are empty.
$alt_storage->delete($name);
$this->assertSame(['collection.sub.another', 'collection.sub.new'], $this->storage->getAllCollectionNames());
// Create configuration in collection called 'collection'. This ensures that
// FileStorage's collection storage works regardless of its use of
// subdirectories.
$parent_storage = $this->storage->createCollection('collection');
$this->assertFalse($parent_storage->exists($name));
$this->assertEquals([], $parent_storage->listAll());
$parent_storage->write($name, $new_data);
$this->assertTrue($result);
$this->assertSame($new_data, $parent_storage->read($name));
$this->assertEquals([$name], $parent_storage->listAll());
$this->assertTrue($parent_storage->exists($name));
$this->assertSame(['collection', 'collection.sub.another', 'collection.sub.new'], $this->storage->getAllCollectionNames());
$parent_storage->deleteAll();
$this->assertSame(['collection.sub.another', 'collection.sub.new'], $this->storage->getAllCollectionNames());
// Test operations on a collection emptied through deletion.
$this->assertFalse($parent_storage->exists($name));
$this->assertEquals([], $parent_storage->listAll());
$this->assertFalse($parent_storage->delete($name));
$this->assertFalse($parent_storage->deleteAll('config_test.'));
$this->assertFalse($parent_storage->deleteAll());
$this->assertFalse($parent_storage->rename($name, 'config_test.another_name'));
// Check that the having an empty collection-less storage does not break
// anything. Before deleting check that the previous delete did not affect
// data in another collection.
$this->assertSame($data, $this->storage->read($name));
$this->storage->delete($name);
$this->assertSame(['collection.sub.another', 'collection.sub.new'], $this->storage->getAllCollectionNames());
}
/**
* Reads configuration data from the storage.
*/
abstract protected function read($name);
/**
* Inserts configuration data in the storage.
*/
abstract protected function insert($name, $data);
/**
* Updates configuration data in the storage.
*/
abstract protected function update($name, $data);
/**
* Deletes configuration data from the storage.
*/
abstract protected function delete($name);
}

View File

@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config\Storage;
use Drupal\Core\Config\DatabaseStorage;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\DatabaseExceptionWrapper;
/**
* Tests DatabaseStorage operations.
*
* @group config
*/
class DatabaseStorageTest extends ConfigStorageTestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->storage = new DatabaseStorage($this->container->get('database'), 'config');
$this->invalidStorage = new DatabaseStorage($this->container->get('database'), 'invalid');
}
/**
* {@inheritdoc}
*/
protected function read($name) {
$data = Database::getConnection()->select('config', 'c')->fields('c', ['data'])->condition('name', $name)->execute()->fetchField();
return unserialize($data);
}
/**
* {@inheritdoc}
*/
protected function insert($name, $data): void {
Database::getConnection()->insert('config')->fields(['name' => $name, 'data' => $data])->execute();
}
/**
* {@inheritdoc}
*/
protected function update($name, $data): void {
Database::getConnection()->update('config')->fields(['data' => $data])->condition('name', $name)->execute();
}
/**
* {@inheritdoc}
*/
protected function delete($name): void {
Database::getConnection()->delete('config')->condition('name', $name)->execute();
}
/**
* Tests that operations throw exceptions if the query fails.
*/
public function testExceptionIsThrownIfQueryFails(): void {
$connection = Database::getConnection();
if ($connection->databaseType() === 'sqlite') {
// See: https://www.drupal.org/project/drupal/issues/3349286
$this->markTestSkipped('SQLite cannot allow detection of exceptions due to double quoting.');
return;
}
Database::getConnection()->schema()->dropTable('config');
// In order to simulate database issue create a table with an incorrect
// specification.
$table_specification = [
'fields' => [
'id' => [
'type' => 'int',
'default' => NULL,
],
],
];
Database::getConnection()->schema()->createTable('config', $table_specification);
try {
$this->storage->exists('config.settings');
$this->fail('Expected exception not thrown from exists()');
}
catch (DatabaseExceptionWrapper) {
// Exception was expected
}
try {
$this->storage->read('config.settings');
$this->fail('Expected exception not thrown from read()');
}
catch (DatabaseExceptionWrapper) {
// Exception was expected
}
try {
$this->storage->readMultiple(['config.settings', 'config.settings2']);
$this->fail('Expected exception not thrown from readMultiple()');
}
catch (DatabaseExceptionWrapper) {
// Exception was expected
}
try {
$this->storage->write('config.settings', ['data' => '']);
$this->fail('Expected exception not thrown from deleteAll()');
}
catch (DatabaseExceptionWrapper) {
// Exception was expected
}
try {
$this->storage->listAll();
$this->fail('Expected exception not thrown from listAll()');
}
catch (DatabaseExceptionWrapper) {
// Exception was expected
}
try {
$this->storage->deleteAll();
$this->fail('Expected exception not thrown from deleteAll()');
}
catch (DatabaseExceptionWrapper) {
// Exception was expected
}
try {
$this->storage->getAllCollectionNames();
$this->fail('Expected exception not thrown from getAllCollectionNames()');
}
catch (DatabaseExceptionWrapper) {
// Exception was expected
}
$this->assertTrue(TRUE);
}
}

View File

@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config\Storage;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Config\UnsupportedDataTypeConfigException;
use Drupal\Core\Serialization\Yaml;
use Drupal\Core\StreamWrapper\PublicStream;
/**
* Tests FileStorage operations.
*
* @group config
*/
class FileStorageTest extends ConfigStorageTestBase {
/**
* A directory to store configuration in.
*
* @var string
*/
protected $directory;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a directory.
$this->directory = PublicStream::basePath() . '/config';
$this->storage = new FileStorage($this->directory);
$this->invalidStorage = new FileStorage($this->directory . '/nonexisting');
// FileStorage::listAll() requires other configuration data to exist.
$this->storage->write('system.performance', $this->config('system.performance')->get());
$this->storage->write('core.extension', ['module' => []]);
}
/**
* {@inheritdoc}
*/
protected function read($name) {
$data = file_get_contents($this->storage->getFilePath($name));
return Yaml::decode($data);
}
/**
* {@inheritdoc}
*/
protected function insert($name, $data): void {
file_put_contents($this->storage->getFilePath($name), $data);
}
/**
* {@inheritdoc}
*/
protected function update($name, $data): void {
file_put_contents($this->storage->getFilePath($name), $data);
}
/**
* {@inheritdoc}
*/
protected function delete($name): void {
unlink($this->storage->getFilePath($name));
}
/**
* Tests the FileStorage::listAll method with a relative and absolute path.
*/
public function testListAll(): void {
$expected_files = [
'core.extension',
'system.performance',
];
$config_files = $this->storage->listAll();
$this->assertSame($expected_files, $config_files, 'Relative path, two config files found.');
// @todo https://www.drupal.org/node/2666954 FileStorage::listAll() is
// case-sensitive. However, \Drupal\Core\Config\DatabaseStorage::listAll()
// is case-insensitive.
$this->assertSame(['system.performance'], $this->storage->listAll('system'), 'The FileStorage::listAll() with prefix works.');
$this->assertSame([], $this->storage->listAll('System'), 'The FileStorage::listAll() is case sensitive.');
}
/**
* Tests UnsupportedDataTypeConfigException.
*/
public function testUnsupportedDataTypeConfigException(): void {
$name = 'core.extension';
$path = $this->storage->getFilePath($name);
$this->expectException(UnsupportedDataTypeConfigException::class);
$this->expectExceptionMessageMatches("@Invalid data type in config $name, found in file $path: @");
file_put_contents($path, PHP_EOL . 'foo : @bar', FILE_APPEND);
$this->storage->read($name);
}
}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config\Storage;
use Drupal\Core\Config\StorageManagerInterface;
use Drupal\Core\Config\ManagedStorage;
use Drupal\Core\Config\MemoryStorage;
/**
* Tests ManagedStorage operations.
*
* @group config
*/
class ManagedStorageTest extends ConfigStorageTestBase implements StorageManagerInterface {
/**
* {@inheritdoc}
*/
public function getStorage() {
// We return a new storage every time to make sure the managed storage
// only calls this once and retains the configuration by itself.
return new MemoryStorage();
}
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->storage = new ManagedStorage($this);
}
/**
* {@inheritdoc}
*/
protected function read($name) {
return $this->storage->read($name);
}
/**
* {@inheritdoc}
*/
protected function insert($name, $data): void {
$this->storage->write($name, $data);
}
/**
* {@inheritdoc}
*/
protected function update($name, $data): void {
$this->storage->write($name, $data);
}
/**
* {@inheritdoc}
*/
protected function delete($name): void {
$this->storage->delete($name);
}
/**
* {@inheritdoc}
*/
public function testInvalidStorage(): void {
$this->markTestSkipped('ManagedStorage cannot be invalid.');
}
}

View File

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config\Storage;
use Drupal\Core\Config\MemoryStorage;
/**
* Tests MemoryStorage operations.
*
* @group config
*/
class MemoryStorageTest extends ConfigStorageTestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->storage = new MemoryStorage();
}
/**
* {@inheritdoc}
*/
protected function read($name) {
return $this->storage->read($name);
}
/**
* {@inheritdoc}
*/
protected function insert($name, $data): void {
$this->storage->write($name, $data);
}
/**
* {@inheritdoc}
*/
protected function update($name, $data): void {
$this->storage->write($name, $data);
}
/**
* {@inheritdoc}
*/
protected function delete($name): void {
$this->storage->delete($name);
}
/**
* {@inheritdoc}
*/
public function testInvalidStorage(): void {
$this->markTestSkipped('MemoryStorage cannot be invalid.');
}
}

View File

@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Config\Storage;
use Drupal\config\StorageReplaceDataWrapper;
use Drupal\Core\Config\StorageInterface;
/**
* Tests StorageReplaceDataWrapper operations.
*
* @group config
*/
class StorageReplaceDataWrapperTest extends ConfigStorageTestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->storage = new StorageReplaceDataWrapper($this->container->get('config.storage'));
}
/**
* {@inheritdoc}
*/
protected function read($name) {
return $this->storage->read($name);
}
/**
* {@inheritdoc}
*/
protected function insert($name, $data): void {
$this->storage->write($name, $data);
}
/**
* {@inheritdoc}
*/
protected function update($name, $data): void {
$this->storage->write($name, $data);
}
/**
* {@inheritdoc}
*/
protected function delete($name): void {
$this->storage->delete($name);
}
/**
* {@inheritdoc}
*/
public function testInvalidStorage(): void {
$this->markTestSkipped('No-op as this test does not make sense');
}
/**
* Tests if new collections created correctly.
*
* @param string $collection
* The collection name.
*
* @dataProvider providerCollections
*/
public function testCreateCollection($collection): void {
$initial_collection_name = $this->storage->getCollectionName();
// Create new storage with given collection and check it is set correctly.
$new_storage = $this->storage->createCollection($collection);
$this->assertSame($collection, $new_storage->getCollectionName());
// Check collection not changed in the current storage instance.
$this->assertSame($initial_collection_name, $this->storage->getCollectionName());
}
/**
* Data provider for testing different collections.
*
* @return array
* Returns an array of collection names.
*/
public static function providerCollections() {
return [
[StorageInterface::DEFAULT_COLLECTION],
['foo.bar'],
];
}
}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Controller;
use Drupal\dblog\Logger\DbLog;
use Drupal\KernelTests\KernelTestBase;
use Drupal\system_test\Controller\BrokenSystemTestController;
use Drupal\system_test\Controller\OptionalServiceSystemTestController;
use Drupal\system_test\Controller\SystemTestController;
use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException;
/**
* Tests \Drupal\Core\Controller\ControllerBase.
*
* @coversDefaultClass \Drupal\Core\Controller\ControllerBase
* @group Controller
*/
class ControllerBaseTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system_test', 'system'];
/**
* @covers ::create
*/
public function testCreate(): void {
/** @var \Drupal\system_test\Controller\SystemTestController $controller */
$controller = $this->container->get('class_resolver')->getInstanceFromDefinition(SystemTestController::class);
$property = new \ReflectionProperty(SystemTestController::class, 'lock');
$this->assertSame($this->container->get('lock'), $property->getValue($controller));
$property = new \ReflectionProperty(SystemTestController::class, 'persistentLock');
$this->assertSame($this->container->get('lock.persistent'), $property->getValue($controller));
$property = new \ReflectionProperty(SystemTestController::class, 'currentUser');
$this->assertSame($this->container->get('current_user'), $property->getValue($controller));
// Test nullables types.
$this->assertSame($this->container->get('page_cache_kill_switch'), $controller->killSwitch);
$this->assertSame($this->container->get('page_cache_kill_switch'), $controller->killSwitch2);
}
/**
* @covers ::create
*/
public function testCreateException(): void {
$this->expectException(AutowiringFailedException::class);
$this->expectExceptionMessage('Cannot autowire service "Drupal\Core\Lock\LockBackendInterface": argument "$lock" of method "Drupal\system_test\Controller\BrokenSystemTestController::_construct()", you should configure its value explicitly.');
$this->container->get('class_resolver')->getInstanceFromDefinition(BrokenSystemTestController::class);
}
/**
* @covers ::create
*/
public function testCreateOptional(): void {
$service = $this->container->get('class_resolver')->getInstanceFromDefinition(OptionalServiceSystemTestController::class);
$this->assertInstanceOf(OptionalServiceSystemTestController::class, $service);
$this->assertNull($service->dbLog);
$this->container->get('module_installer')->install(['dblog']);
$service = $this->container->get('class_resolver')->getInstanceFromDefinition(OptionalServiceSystemTestController::class);
$this->assertInstanceOf(OptionalServiceSystemTestController::class, $service);
$this->assertInstanceOf(DbLog::class, $service->dbLog);
}
}

View File

@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
namespace Drupal\KernelTests\Core\Database;
/**
* Tests the hook_query_alter capabilities of the Select builder.
*
* @group Database
* @see database_test_query_alter()
*/
class AlterTest extends DatabaseTestBase {
/**
* Tests that we can do basic alters.
*/
public function testSimpleAlter(): void {
$query = $this->connection->select('test');
$query->addField('test', 'name');
$query->addField('test', 'age', 'age');
$query->addTag('database_test_alter_add_range');
$result = $query->execute()->fetchAll();
$this->assertCount(2, $result, 'Returned the correct number of rows.');
}
/**
* Tests that we can alter the joins on a query.
*/
public function testAlterWithJoin(): void {
$query = $this->connection->select('test_task');
$tid_field = $query->addField('test_task', 'tid');
$task_field = $query->addField('test_task', 'task');
$query->orderBy($task_field);
$query->addTag('database_test_alter_add_join');
$result = $query->execute();
$records = $result->fetchAll();
$this->assertCount(2, $records, 'Returned the correct number of rows.');
$this->assertEquals('George', $records[0]->name, 'Correct data retrieved.');
$this->assertEquals(4, $records[0]->{$tid_field}, 'Correct data retrieved.');
$this->assertEquals('sing', $records[0]->{$task_field}, 'Correct data retrieved.');
$this->assertEquals('George', $records[1]->name, 'Correct data retrieved.');
$this->assertEquals(5, $records[1]->{$tid_field}, 'Correct data retrieved.');
$this->assertEquals('sleep', $records[1]->{$task_field}, 'Correct data retrieved.');
}
/**
* Tests that we can alter a query's conditionals.
*/
public function testAlterChangeConditional(): void {
$query = $this->connection->select('test_task');
$tid_field = $query->addField('test_task', 'tid');
$pid_field = $query->addField('test_task', 'pid');
$task_field = $query->addField('test_task', 'task');
$people_alias = $query->join('test', 'people', "[test_task].[pid] = [people].[id]");
$name_field = $query->addField($people_alias, 'name', 'name');
$query->condition('test_task.tid', '1');
$query->orderBy($tid_field);
$query->addTag('database_test_alter_change_conditional');
$result = $query->execute();
$records = $result->fetchAll();
$this->assertCount(1, $records, 'Returned the correct number of rows.');
$this->assertEquals('John', $records[0]->{$name_field}, 'Correct data retrieved.');
$this->assertEquals(2, $records[0]->{$tid_field}, 'Correct data retrieved.');
$this->assertEquals(1, $records[0]->{$pid_field}, 'Correct data retrieved.');
$this->assertEquals('sleep', $records[0]->{$task_field}, 'Correct data retrieved.');
}
/**
* Tests that we can alter the fields of a query.
*/
public function testAlterChangeFields(): void {
$query = $this->connection->select('test');
$name_field = $query->addField('test', 'name');
$age_field = $query->addField('test', 'age', 'age');
$query->orderBy('name');
$query->addTag('database_test_alter_change_fields');
$record = $query->execute()->fetch();
$this->assertEquals('George', $record->{$name_field}, 'Correct data retrieved.');
$this->assertFalse(isset($record->$age_field), 'Age field not found, as intended.');
}
/**
* Tests that we can alter expressions in the query.
*/
public function testAlterExpression(): void {
$query = $this->connection->select('test');
$name_field = $query->addField('test', 'name');
$age_field = $query->addExpression("[age]*2", 'double_age');
$query->condition('age', 27);
$query->addTag('database_test_alter_change_expressions');
$result = $query->execute();
// Ensure that we got the right record.
$record = $result->fetch();
$this->assertEquals('George', $record->{$name_field}, 'Fetched name is correct.');
$this->assertEquals(27 * 3, $record->{$age_field}, 'Fetched age expression is correct.');
}
/**
* Tests that we can remove a range() value from a query.
*
* This also tests hook_query_TAG_alter().
*/
public function testAlterRemoveRange(): void {
$query = $this->connection->select('test');
$query->addField('test', 'name');
$query->addField('test', 'age', 'age');
$query->range(0, 2);
$query->addTag('database_test_alter_remove_range');
$num_records = count($query->execute()->fetchAll());
$this->assertEquals(4, $num_records, 'Returned the correct number of rows.');
}
/**
* Tests that we can do basic alters on subqueries.
*/
public function testSimpleAlterSubquery(): void {
// Create a sub-query with an alter tag.
$subquery = $this->connection->select('test', 'p');
$subquery->addField('p', 'name');
$subquery->addField('p', 'id');
// Pick out George.
$subquery->condition('age', 27);
$subquery->addExpression("[age]*2", 'double_age');
// This query alter should change it to age * 3.
$subquery->addTag('database_test_alter_change_expressions');
// Create a main query and join to sub-query.
$query = $this->connection->select('test_task', 'tt');
$query->join($subquery, 'pq', '[pq].[id] = [tt].[pid]');
$age_field = $query->addField('pq', 'double_age');
$name_field = $query->addField('pq', 'name');
$record = $query->execute()->fetch();
$this->assertEquals('George', $record->{$name_field}, 'Fetched name is correct.');
$this->assertEquals(27 * 3, $record->{$age_field}, 'Fetched age expression is correct.');
}
}

Some files were not shown because too many files have changed in this diff Show More