Issue #3382581 by phenaproxima, borisson_, larowlan, Wim Leers, quietone: Add new `EntityBundleExists` constraint

merge-requests/6419/head
Lee Rowlands 2024-01-31 17:52:37 +10:00
parent d75377535a
commit 89ec081a50
No known key found for this signature in database
GPG Key ID: 2B829A3DF9204DC4
30 changed files with 754 additions and 204 deletions

View File

@ -499,6 +499,8 @@ field_config_base:
bundle:
type: string
label: 'Bundle'
constraints:
EntityBundleExists: '%parent.entity_type'
label:
type: required_label
label: 'Label'

View File

@ -54,6 +54,8 @@ core.entity_view_display.*.*.*:
bundle:
type: string
label: 'Bundle'
constraints:
EntityBundleExists: '%parent.targetEntityType'
mode:
type: string
label: 'View or form mode machine name'
@ -115,6 +117,8 @@ core.entity_form_display.*.*.*:
bundle:
type: string
label: 'Bundle'
constraints:
EntityBundleExists: '%parent.targetEntityType'
mode:
type: string
label: 'View or form mode machine name'

View File

@ -0,0 +1,124 @@
<?php
declare(strict_types = 1);
namespace Drupal\Core\Config\Schema;
use Drupal\Core\TypedData\TypedDataInterface;
/**
* Provides helper methods for resolving config schema types.
*
* @internal
* This is an internal part of the config schema system and may be changed or
* removed any time. External code should not interact with this class.
*/
class TypeResolver {
/**
* Replaces dynamic type expressions in configuration type.
*
* The configuration type name may contain one or more expressions to be
* replaced, enclosed in square brackets like '[name]' or '[%parent.id]' and
* will follow the replacement rules defined by the resolveExpression()
* method.
*
* @param string $name
* Configuration type, potentially with expressions in square brackets.
* @param array $data
* Configuration data for the element.
*
* @return string
* Configuration type name with all expressions resolved.
*/
public static function resolveDynamicTypeName(string $name, mixed $data): string {
if (preg_match_all("/\[(.*)\]/U", $name, $matches)) {
// Build our list of '[value]' => replacement.
$replace = [];
foreach (array_combine($matches[0], $matches[1]) as $key => $value) {
$replace[$key] = self::resolveExpression($value, $data);
}
return strtr($name, $replace);
}
return $name;
}
/**
* Resolves a dynamic type expression using configuration data.
*
* Dynamic type names are nested configuration keys containing expressions to
* be replaced by the value at the property path that the expression is
* pointing at. The expression may contain the following special strings:
* - '%key', will be replaced by the element's key.
* - '%parent', to reference the parent element.
* - '%type', to reference the schema definition type. Can only be used in
* combination with %parent.
*
* There may be nested configuration keys separated by dots or more complex
* patterns like '%parent.name' which references the 'name' value of the
* parent element.
*
* Example expressions:
* - 'name.subkey', indicates a nested value of the current element.
* - '%parent.name', will be replaced by the 'name' value of the parent.
* - '%parent.%key', will be replaced by the parent element's key.
* - '%parent.%type', will be replaced by the schema type of the parent.
* - '%parent.%parent.%type', will be replaced by the schema type of the
* parent's parent.
*
* @param string $expression
* Expression to be resolved.
* @param array|\Drupal\Core\TypedData\TypedDataInterface $data
* Configuration data for the element.
*
* @return string
* The value the expression resolves to, or the given expression if it
* cannot be resolved.
*
* @todo Validate the expression in https://www.drupal.org/project/drupal/issues/3392903
*/
public static function resolveExpression(string $expression, array|TypedDataInterface $data): string {
if ($data instanceof TypedDataInterface) {
$data = [
'%parent' => $data->getParent(),
'%key' => $data->getName(),
'%type' => $data->getDataDefinition()->getDataType(),
];
}
$parts = explode('.', $expression);
// Process each value part, one at a time.
while ($name = array_shift($parts)) {
if (!is_array($data) || !isset($data[$name])) {
// Key not found, return original value
return $expression;
}
if (!$parts) {
$expression = $data[$name];
if (is_bool($expression)) {
$expression = (int) $expression;
}
// If no more parts left, this is the final property.
return (string) $expression;
}
// Get nested value and continue processing.
if ($name == '%parent') {
/** @var \Drupal\Core\Config\Schema\ArrayElement $parent */
// Switch replacement values with values from the parent.
$parent = $data['%parent'];
$data = $parent->getValue();
$data['%type'] = $parent->getDataDefinition()->getDataType();
// The special %parent and %key values now need to point one level up.
if ($new_parent = $parent->getParent()) {
$data['%parent'] = $new_parent;
$data['%key'] = $new_parent->getName();
}
continue;
}
$data = $data[$name];
}
// Return the original value
return $expression;
}
}

View File

@ -7,6 +7,7 @@ use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\Schema\ConfigSchemaAlterException;
use Drupal\Core\Config\Schema\ConfigSchemaDiscovery;
use Drupal\Core\Config\Schema\TypeResolver;
use Drupal\Core\Config\Schema\SequenceDataDefinition;
use Drupal\Core\DependencyInjection\ClassResolverInterface;
use Drupal\Core\Config\Schema\Undefined;
@ -108,7 +109,7 @@ class TypedConfigManager extends TypedDataManager implements TypedConfigManagerI
if (isset($name)) {
$replace['%key'] = $name;
}
$type = $this->resolveDynamicTypeName($type, $replace);
$type = TypeResolver::resolveDynamicTypeName($type, $replace);
// Remove the type from the definition so that it is replaced with the
// concrete type from schema definitions.
unset($definition['type']);
@ -288,7 +289,7 @@ class TypedConfigManager extends TypedDataManager implements TypedConfigManagerI
// Replace dynamic portions of the definition type.
if (!empty($replacements) && strpos($definition['type'], ']')) {
$sub_type = $this->determineType($this->resolveDynamicTypeName($definition['type'], $replacements), $definitions);
$sub_type = $this->determineType(TypeResolver::resolveDynamicTypeName($definition['type'], $replacements), $definitions);
$sub_definition = $definitions[$sub_type];
if (isset($definitions[$sub_type]['type'])) {
$sub_merge = $this->getDefinition($definitions[$sub_type]['type'], $exception_on_invalid);
@ -395,45 +396,8 @@ class TypedConfigManager extends TypedDataManager implements TypedConfigManagerI
/**
* Replaces dynamic type expressions in configuration type.
*
* The configuration type name may contain one or more expressions to be
* replaced, enclosed in square brackets like '[name]' or '[%parent.id]' and
* will follow the replacement rules defined by the resolveExpression()
* method.
*
* @param string $type
* Configuration type, potentially with expressions in square brackets.
* @param array $data
* Configuration data for the element.
*
* @return string
* Configuration type name with all expressions resolved.
*/
protected function resolveDynamicTypeName(string $type, array $data): string {
// Parse the expressions in the dynamic type, if any.
if (preg_match_all("/\[(.*)\]/U", $type, $matches)) {
// Build our list of '[value]' => replacement.
$replace = [];
foreach (array_combine($matches[0], $matches[1]) as $key => $value) {
$replace[$key] = $this->resolveExpression($value, $data);
}
return strtr($type, $replace);
}
else {
// No expressions: nothing to resolve.
return $type;
}
}
/**
* Replaces dynamic type expressions in configuration type.
*
* The configuration type name may contain one or more expressions to be
* replaced, enclosed in square brackets like '[name]' or '[%parent.id]' and
* will follow the replacement rules defined by the resolveExpression()
* method.
*
* @param string $name
* Configuration type, potentially with expressions in square brackets.
* Configuration type, potentially with expressions in square brackets.f
* @param array $data
* Configuration data for the element.
*
@ -441,113 +405,19 @@ class TypedConfigManager extends TypedDataManager implements TypedConfigManagerI
* Configuration type name with all expressions resolved.
*
* @deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use
* ::resolveDynamicTypeName() instead.
* \Drupal\Core\Config\Schema\TypeResolver::resolveDynamicTypeName::resolveDynamicTypeName()
* instead.
*
* @see https://www.drupal.org/node/3408266
*/
protected function replaceName($name, $data) {
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use ::resolveDynamicTypeName() instead. See https://www.drupal.org/node/3408266', E_USER_DEPRECATED);
return $this->resolveDynamicTypeName($name, $data);
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use \Drupal\Core\Config\Schema\TypeResolver::resolveDynamicTypeName() instead. See https://www.drupal.org/node/3408266', E_USER_DEPRECATED);
return TypeResolver::resolveDynamicTypeName($name, $data);
}
/**
* Resolves a dynamic type expression using configuration data.
*
* Dynamic type names are nested configuration keys containing expressions to
* be replaced by the value at the property path that the expression is
* pointing at. The expression may contain the following special strings:
* - '%key', will be replaced by the element's key.
* - '%parent', to reference the parent element.
* - '%type', to reference the schema definition type. Can only be used in
* combination with %parent.
*
* There may be nested configuration keys separated by dots or more complex
* patterns like '%parent.name' which references the 'name' value of the
* parent element.
*
* Example expressions:
* - 'name.subkey', indicates a nested value of the current element.
* - '%parent.name', will be replaced by the 'name' value of the parent.
* - '%parent.%key', will be replaced by the parent element's key.
* - '%parent.%type', will be replaced by the schema type of the parent.
* - '%parent.%parent.%type', will be replaced by the schema type of the
* parent's parent.
*
* @param string $expression
* Expression to be resolved.
* @param array $data
* Configuration data for the element.
*
* @return string
* The value the expression resolves to, or the given expression if it
* cannot be resolved.
*
* @todo Validate the expression in https://www.drupal.org/project/drupal/issues/3392903
*/
protected function resolveExpression(string $expression, array $data): string {
assert(!str_contains($expression, '[') && !str_contains($expression, ']'));
$parts = explode('.', $expression);
// Process each value part, one at a time.
while ($name = array_shift($parts)) {
if (!is_array($data) || !isset($data[$name])) {
// Key not found, return original value
return $expression;
}
elseif (!$parts) {
$expression = $data[$name];
if (is_bool($expression)) {
$expression = (int) $expression;
}
// If no more parts left, this is the final property.
return (string) $expression;
}
else {
// Get nested value and continue processing.
if ($name == '%parent') {
/** @var \Drupal\Core\Config\Schema\ArrayElement $parent */
// Switch replacement values with values from the parent.
$parent = $data['%parent'];
$data = $parent->getValue();
$data['%type'] = $parent->getDataDefinition()->getDataType();
// The special %parent and %key values now need to point one level up.
if ($new_parent = $parent->getParent()) {
$data['%parent'] = $new_parent;
$data['%key'] = $new_parent->getName();
}
}
else {
$data = $data[$name];
}
}
}
// Satisfy PHPStan, which cannot interpret the loop.
return $expression;
}
/**
* Resolves a dynamic type expression using configuration data.
*
* Dynamic type names are nested configuration keys containing expressions to
* be replaced by the value at the property path that the expression is
* pointing at. The expression may contain the following special strings:
* - '%key', will be replaced by the element's key.
* - '%parent', to reference the parent element.
* - '%type', to reference the schema definition type. Can only be used in
* combination with %parent.
*
* There may be nested configuration keys separated by dots or more complex
* patterns like '%parent.name' which references the 'name' value of the
* parent element.
*
* Example expressions:
* - 'name.subkey', indicates a nested value of the current element.
* - '%parent.name', will be replaced by the 'name' value of the parent.
* - '%parent.%key', will be replaced by the parent element's key.
* - '%parent.%type', will be replaced by the schema type of the parent.
* - '%parent.%parent.%type', will be replaced by the schema type of the
* parent's parent.
*
* @param string $value
* Expression to be resolved.
* @param array $data
@ -558,13 +428,14 @@ class TypedConfigManager extends TypedDataManager implements TypedConfigManagerI
* cannot be resolved.
*
* @deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use
* ::resolveExpression() instead.
* \Drupal\Core\Config\Schema\TypeResolver::resolveDynamicTypeName::resolveExpression()
* instead.
*
* @see https://www.drupal.org/node/3408266
*/
protected function replaceVariable($value, $data) {
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use ::resolveExpression() instead. See https://www.drupal.org/node/3408266', E_USER_DEPRECATED);
return $this->resolveExpression($value, $data);
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use \Drupal\Core\Config\Schema\TypeResolver::resolveExpression() instead. See https://www.drupal.org/node/3408266', E_USER_DEPRECATED);
return TypeResolver::resolveExpression($value, $data);
}
/**
@ -608,4 +479,48 @@ class TypedConfigManager extends TypedDataManager implements TypedConfigManagerI
return $this->create($data_definition, $config_data, $config_name);
}
/**
* Resolves a dynamic type name.
*
* @param string $type
* Configuration type, potentially with expressions in square brackets.
* @param array $data
* Configuration data for the element.
*
* @return string
* Configuration type name with all expressions resolved.
*
* @deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use
* \Drupal\Core\Config\Schema\TypeResolver::resolveDynamicTypeName()
* instead.
*
* @see https://www.drupal.org/node/3413264
*/
protected function resolveDynamicTypeName(string $type, array $data): string {
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use \Drupal\Core\Config\Schema\TypeResolver::' . __FUNCTION__ . '() instead. See https://www.drupal.org/node/3413264', E_USER_DEPRECATED);
return TypeResolver::resolveDynamicTypeName($type, $data);
}
/**
* Resolves a dynamic expression.
*
* @param string $expression
* Expression to be resolved.
* @param array|\Drupal\Core\TypedData\TypedDataInterface $data
* Configuration data for the element.
*
* @return string
* The value the expression resolves to, or the given expression if it
* cannot be resolved.
*
* @deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use
* \Drupal\Core\Config\Schema\TypeResolver::resolveExpression() instead.
*
* @see https://www.drupal.org/node/3413264
*/
protected function resolveExpression(string $expression, array $data): string {
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use \Drupal\Core\Config\Schema\TypeResolver::' . __FUNCTION__ . '() instead. See https://www.drupal.org/node/3413264', E_USER_DEPRECATED);
return TypeResolver::resolveExpression($expression, $data);
}
}

View File

@ -7,6 +7,9 @@ use Symfony\Component\Validator\Constraint;
/**
* Checks if a value is a valid entity type.
*
* This differs from the `EntityBundleExists` constraint in that checks that the
* validated value is an *entity* of a particular bundle.
*
* @Constraint(
* id = "Bundle",
* label = @Translation("Bundle", context = "Validation"),

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
/**
* Checks if a bundle exists on a certain content entity type.
*
* This differs from the `Bundle` constraint in that checks that the validated
* value is the *name of a bundle* of a particular entity type.
*
* @Constraint(
* id = "EntityBundleExists",
* label = @Translation("Entity bundle exists", context = "Validation"),
* type = "entity",
* )
*/
class EntityBundleExistsConstraint extends Constraint {
/**
* The error message if validation fails.
*
* @var string
*/
public $message = "The '@bundle' bundle does not exist on the '@entity_type_id' entity type.";
/**
* The entity type ID which should have the given bundle.
*
* This can contain variable values (e.g., `%parent`) that will be replaced.
*
* @see \Drupal\Core\Config\Schema\TypeResolver::replaceVariable()
*
* @var string
*/
public string $entityTypeId;
/**
* {@inheritdoc}
*/
public function getDefaultOption() {
return 'entityTypeId';
}
/**
* {@inheritdoc}
*/
public function getRequiredOptions() {
return ['entityTypeId'];
}
}

View File

@ -0,0 +1,57 @@
<?php
declare(strict_types = 1);
namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
use Drupal\Core\Config\Schema\TypeResolver;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
/**
* Validates that a bundle exists on a certain content entity type.
*/
class EntityBundleExistsConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
/**
* Constructs an EntityBundleExistsConstraintValidator object.
*
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundleInfo
* The entity type bundle info service.
*/
public function __construct(private readonly EntityTypeBundleInfoInterface $bundleInfo) {}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get(EntityTypeBundleInfoInterface::class),
);
}
/**
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint) {
assert($constraint instanceof EntityBundleExistsConstraint);
if (!is_string($value)) {
throw new UnexpectedTypeException($value, 'string');
}
// Resolve any dynamic tokens, like %parent, in the entity type ID.
$entity_type_id = TypeResolver::resolveDynamicTypeName("[$constraint->entityTypeId]", $this->context->getObject());
if (!array_key_exists($value, $this->bundleInfo->getBundleInfo($entity_type_id))) {
$this->context->addViolation($constraint->message, [
'@bundle' => $value,
'@entity_type_id' => $entity_type_id,
]);
}
}
}

View File

@ -532,6 +532,10 @@ class ContactSitewideTest extends BrowserTestBase {
$edit += $third_party_settings;
$this->drupalGet('admin/structure/contact/add');
$this->submitForm($edit, 'Save');
// Ensure the statically cached bundle info is aware of the contact form
// that was just created in the UI.
$this->container->get('entity_type.bundle.info')->clearCachedBundles();
}
/**

View File

@ -54,6 +54,9 @@ class NodeAccessTest extends ModerationStateTestBase {
parent::setUp();
$this->drupalLogin($this->adminUser);
$this->createContentTypeFromUi('Moderated content', 'moderated_content', FALSE);
// Ensure the statically cached entity bundle info is aware of the content
// type that was just created in the UI.
$this->container->get('entity_type.bundle.info')->clearCachedBundles();
$this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content');
// Add the private field to the node type.

View File

@ -66,6 +66,10 @@ class SelectionTest extends BrowserTestBase {
$this->nodes[$node->id()] = $node;
}
// Ensure the bundle to which the field is attached actually exists, or we
// will get config validation errors.
entity_test_create_bundle('test_bundle');
// Create an entity reference field.
$handler_settings = [
'view' => [

View File

@ -669,6 +669,9 @@ class FormTest extends FieldTestBase {
$user = $this->drupalCreateUser(['administer entity_test content']);
$this->drupalLogin($user);
// Ensure that the 'bar' bundle exists, to avoid config validation errors.
entity_test_create_bundle('bar', entity_type: 'entity_test_base_field_display');
FieldStorageConfig::create([
'entity_type' => 'entity_test_base_field_display',
'field_name' => 'foo',

View File

@ -2,8 +2,11 @@
namespace Drupal\Tests\field\Kernel\Entity;
use Drupal\entity_test\Entity\EntityTestBundle;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
/**
* Tests validation of field_config entities.
@ -11,7 +14,14 @@ use Drupal\field\Entity\FieldStorageConfig;
* @group field
* @group #slow
*/
class FieldConfigValidationTest extends FieldStorageConfigValidationTest {
class FieldConfigValidationTest extends ConfigEntityValidationTestBase {
use ContentTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['field', 'node', 'entity_test', 'text', 'user'];
/**
* {@inheritdoc}
@ -19,14 +29,14 @@ class FieldConfigValidationTest extends FieldStorageConfigValidationTest {
protected function setUp(): void {
parent::setUp();
// The field storage was created in the parent method.
$field_storage = $this->entity;
$this->installConfig('node');
$this->createContentType(['type' => 'one']);
$this->createContentType(['type' => 'another']);
$this->entity = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'user',
]);
$this->entity->save();
EntityTestBundle::create(['id' => 'one'])->save();
EntityTestBundle::create(['id' => 'another'])->save();
$this->entity = FieldConfig::loadByName('node', 'one', 'body');
}
/**
@ -87,6 +97,17 @@ class FieldConfigValidationTest extends FieldStorageConfigValidationTest {
$this->assertValidationErrors([]);
}
/**
* Tests that the target bundle of the field is checked.
*/
public function testTargetBundleMustExist(): void {
$this->entity->set('bundle', 'nope');
$this->assertValidationErrors([
'' => "The 'bundle' property cannot be changed.",
'bundle' => "The 'nope' bundle does not exist on the 'node' entity type.",
]);
}
/**
* {@inheritdoc}
*/
@ -96,7 +117,10 @@ class FieldConfigValidationTest extends FieldStorageConfigValidationTest {
// settings from the *old* field_type won't match the config schema for the
// settings of the *new* field_type.
$this->entity->set('settings', []);
parent::testImmutableProperties($valid_values);
parent::testImmutableProperties([
'entity_type' => 'entity_test_with_bundle',
'bundle' => 'another',
]);
}
/**

View File

@ -36,7 +36,7 @@ class FieldEntitySettingsTest extends KernelTestBase {
/** @var \Drupal\field\FieldStorageConfigInterface $field_storage */
$field_storage = FieldStorageConfig::create([
'type' => 'integer',
'entity_type' => 'entity_test',
'entity_type' => 'entity_test_with_bundle',
'field_name' => 'test',
]);
$field = FieldConfig::create([
@ -95,10 +95,10 @@ class FieldEntitySettingsTest extends KernelTestBase {
$field_storage = FieldStorageConfig::create([
'field_name' => 'test_reference',
'type' => 'entity_reference',
'entity_type' => 'entity_test',
'entity_type' => 'entity_test_with_bundle',
'cardinality' => 1,
'settings' => [
'target_type' => 'entity_test',
'target_type' => 'entity_test_with_bundle',
],
]);
$field_storage->save();
@ -111,10 +111,10 @@ class FieldEntitySettingsTest extends KernelTestBase {
'handler' => 'default',
],
]);
$this->assertSame('default:entity_test', $field->getSetting('handler'));
$this->assertSame('default:entity_test_with_bundle', $field->getSetting('handler'));
// If the handler is changed, it should be normalized again on pre-save.
$field->setSetting('handler', 'default')->save();
$this->assertSame('default:entity_test', $field->getSetting('handler'));
$this->assertSame('default:entity_test_with_bundle', $field->getSetting('handler'));
}
}

View File

@ -73,6 +73,10 @@ class SelectionTest extends KernelTestBase {
$this->nodes[$node->id()] = $node;
}
// Ensure the bundle to which the field is attached actually exists, or we
// will get config validation errors.
entity_test_create_bundle('test_bundle');
// Create an entity reference field.
$handler_settings = [
'view' => [

View File

@ -84,8 +84,8 @@ class FieldAttachStorageTest extends FieldKernelTestBase {
1 => 'test_bundle_1',
2 => 'test_bundle_2',
];
entity_test_create_bundle($bundles[1]);
entity_test_create_bundle($bundles[2]);
entity_test_create_bundle($bundles[1], entity_type: $entity_type);
entity_test_create_bundle($bundles[2], entity_type: $entity_type);
// Define 3 fields:
// - field_1 is in bundle_1 and bundle_2,
// - field_2 is in bundle_1,
@ -361,7 +361,12 @@ class FieldAttachStorageTest extends FieldKernelTestBase {
$this->assertCount(4, $entity->{$this->fieldTestData->field_name}, 'First field got loaded');
$this->assertCount(1, $entity->{$field_name}, 'Second field got loaded');
// Delete the bundle.
// Delete the bundle. The form display has to be deleted first to prevent
// schema errors when fields attached to the deleted bundle are themselves
// deleted, which triggers an update of the form display.
$this->container->get('entity_display.repository')
->getFormDisplay($entity_type, $this->fieldTestData->field->getTargetBundle())
->delete();
entity_test_delete_bundle($this->fieldTestData->field->getTargetBundle(), $entity_type);
// Verify no data gets loaded

View File

@ -26,6 +26,8 @@ class FieldImportChangeTest extends FieldKernelTestBase {
* Tests importing an updated field.
*/
public function testImportChange() {
entity_test_create_bundle('test_bundle');
$this->installConfig(['field_test_config']);
$field_storage_id = 'field_test_import';
$field_id = "entity_test.entity_test.$field_storage_id";

View File

@ -28,6 +28,8 @@ class FieldImportDeleteTest extends FieldKernelTestBase {
* Tests deleting field storages and fields as part of config import.
*/
public function testImportDelete() {
entity_test_create_bundle('test_bundle');
$this->installConfig(['field_test_config']);
// At this point there are 5 field configuration objects in the active
// storage.

View File

@ -117,6 +117,8 @@ language.content_settings.*.*:
target_bundle:
type: string
label: 'Bundle'
constraints:
EntityBundleExists: '%parent.target_entity_type_id'
default_langcode:
type: langcode
label: 'Default language'

View File

@ -106,6 +106,10 @@ class LanguageConfigurationElementTest extends BrowserTestBase {
])->save();
}
// Ensure the bundles under test exist, to avoid config validation errors.
entity_test_create_bundle('custom_bundle');
entity_test_create_bundle('some_bundle');
// Fixed language.
ContentLanguageSettings::loadByEntityTypeBundle('entity_test', 'custom_bundle')
->setLanguageAlterable(TRUE)

View File

@ -2,8 +2,10 @@
namespace Drupal\Tests\language\Kernel;
use Drupal\entity_test\Entity\EntityTestBundle;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
use Drupal\language\Entity\ContentLanguageSettings;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
/**
* Tests validation of content_language_settings entities.
@ -13,10 +15,19 @@ use Drupal\language\Entity\ContentLanguageSettings;
*/
class ContentLanguageSettingsValidationTest extends ConfigEntityValidationTestBase {
use ContentTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['language', 'user'];
protected static $modules = [
'entity_test',
'field',
'language',
'node',
'text',
'user',
];
/**
* {@inheritdoc}
@ -28,12 +39,40 @@ class ContentLanguageSettingsValidationTest extends ConfigEntityValidationTestBa
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig('node');
$this->createContentType(['type' => 'alpha']);
$this->createContentType(['type' => 'bravo']);
EntityTestBundle::create(['id' => 'alpha'])->save();
EntityTestBundle::create(['id' => 'bravo'])->save();
$this->entity = ContentLanguageSettings::create([
'target_entity_type_id' => 'user',
'target_bundle' => 'user',
'target_entity_type_id' => 'node',
'target_bundle' => 'alpha',
]);
$this->entity->save();
}
/**
* Tests that the target bundle of the language content settings is checked.
*/
public function testTargetBundleMustExist(): void {
$this->entity->set('target_bundle', 'superhero');
$this->assertValidationErrors([
'' => "The 'target_bundle' property cannot be changed.",
'target_bundle' => "The 'superhero' bundle does not exist on the 'node' entity type.",
]);
}
/**
* {@inheritdoc}
*/
public function testImmutableProperties(array $valid_values = []): void {
parent::testImmutableProperties([
'target_entity_type_id' => 'entity_test_with_bundle',
'target_bundle' => 'bravo',
]);
}
}

View File

@ -3,8 +3,11 @@
namespace Drupal\Tests\layout_builder\Kernel;
use Drupal\Core\Entity\Entity\EntityViewMode;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\entity_test\Entity\EntityTestBundle;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
/**
* Tests validation of Layout Builder's entity_view_display entities.
@ -14,10 +17,19 @@ use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
*/
class LayoutBuilderEntityViewDisplayValidationTest extends ConfigEntityValidationTestBase {
use ContentTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['layout_builder', 'user'];
protected static $modules = [
'entity_test',
'field',
'layout_builder',
'node',
'text',
'user',
];
/**
* {@inheritdoc}
@ -25,18 +37,22 @@ class LayoutBuilderEntityViewDisplayValidationTest extends ConfigEntityValidatio
protected function setUp(): void {
parent::setUp();
$this->installConfig('node');
$this->createContentType(['type' => 'one']);
$this->createContentType(['type' => 'two']);
EntityTestBundle::create(['id' => 'one'])->save();
EntityTestBundle::create(['id' => 'two'])->save();
EntityViewMode::create([
'id' => 'user.layout',
'id' => 'node.layout',
'label' => 'Layout',
'targetEntityType' => 'user',
'targetEntityType' => 'node',
])->save();
$this->entity = LayoutBuilderEntityViewDisplay::create([
'mode' => 'layout',
'label' => 'Layout',
'targetEntityType' => 'user',
'bundle' => 'user',
]);
$this->entity = $this->container->get(EntityDisplayRepositoryInterface::class)
->getViewDisplay('node', 'one', 'layout');
$this->assertInstanceOf(LayoutBuilderEntityViewDisplay::class, $this->entity);
$this->entity->save();
}
@ -49,4 +65,14 @@ class LayoutBuilderEntityViewDisplayValidationTest extends ConfigEntityValidatio
$this->markTestSkipped();
}
/**
* {@inheritdoc}
*/
public function testImmutableProperties(array $valid_values = []): void {
parent::testImmutableProperties([
'targetEntityType' => 'entity_test_with_bundle',
'bundle' => 'two',
]);
}
}

View File

@ -83,6 +83,9 @@ class MediaLibraryDisplayModeTest extends BrowserTestBase {
// Display modes are created on install.
$this->container->get('module_installer')->install(['media_library']);
// The container was rebuilt during module installation, so ensure we have
// an up-to-date reference to it.
$this->container = $this->kernel->getContainer();
// For a non-image media type without a mapped name field, the media_library
// form mode should only contain the name field.
@ -183,6 +186,10 @@ class MediaLibraryDisplayModeTest extends BrowserTestBase {
$this->assertFormDisplay($type_id, FALSE, FALSE);
$this->assertViewDisplay($type_id, 'medium');
// Now that all our media types have been created, ensure the bundle info
// cache is up-to-date.
$this->container->get('entity_type.bundle.info')->clearCachedBundles();
// Delete a form and view display.
EntityFormDisplay::load('media.type_one.media_library')->delete();
EntityViewDisplay::load('media.type_one.media_library')->delete();

View File

@ -4,8 +4,8 @@ namespace Drupal\Tests\media_library\Kernel;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultReasonInterface;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\entity_test\Entity\EntityTestBundle;
use Drupal\entity_test\Entity\EntityTestWithBundle;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\KernelTests\KernelTestBase;
@ -50,7 +50,7 @@ class MediaLibraryAccessTest extends KernelTestBase {
$this->installEntitySchema('user');
$this->installEntitySchema('file');
$this->installSchema('file', 'file_usage');
$this->installEntitySchema('entity_test');
$this->installEntitySchema('entity_test_with_bundle');
$this->installEntitySchema('filter_format');
$this->installEntitySchema('media');
$this->installConfig([
@ -67,7 +67,7 @@ class MediaLibraryAccessTest extends KernelTestBase {
$field_storage = FieldStorageConfig::create([
'type' => 'entity_reference',
'field_name' => 'field_test_media',
'entity_type' => 'entity_test',
'entity_type' => 'entity_test_with_bundle',
'settings' => [
'target_type' => 'media',
],
@ -92,7 +92,7 @@ class MediaLibraryAccessTest extends KernelTestBase {
// Create a media library state to test access.
$state = MediaLibraryState::create('media_library.opener.field_widget', ['file', 'image'], 'file', 2, [
'entity_type_id' => 'entity_test',
'entity_type_id' => 'entity_test_with_bundle',
'bundle' => 'test',
'field_name' => 'field_test_media',
]);
@ -187,7 +187,7 @@ class MediaLibraryAccessTest extends KernelTestBase {
/** @var \Drupal\media_library\MediaLibraryUiBuilder $ui_builder */
$ui_builder = $this->container->get('media_library.ui_builder');
$forbidden_entity = EntityTest::create([
$forbidden_entity = EntityTestWithBundle::create([
'type' => 'test',
// This label will automatically cause an access denial.
// @see \Drupal\entity_test\EntityTestAccessControlHandler::checkAccess()
@ -206,7 +206,7 @@ class MediaLibraryAccessTest extends KernelTestBase {
$access_result = $ui_builder->checkAccess($this->createUser(), $state);
$this->assertAccess($access_result, FALSE, NULL, [], ['url.query_args']);
$neutral_entity = EntityTest::create([
$neutral_entity = EntityTestWithBundle::create([
'type' => 'test',
// This label will result in neutral access.
// @see \Drupal\entity_test\EntityTestAccessControlHandler::checkAccess()
@ -262,7 +262,7 @@ class MediaLibraryAccessTest extends KernelTestBase {
public function testFieldWidgetEntityFieldAccess(string $field_type) {
$field_storage = FieldStorageConfig::create([
'type' => $field_type,
'entity_type' => 'entity_test',
'entity_type' => 'entity_test_with_bundle',
// The media_library_test module will deny access to this field.
// @see media_library_test_entity_field_access()
'field_name' => 'field_media_no_access',
@ -286,7 +286,7 @@ class MediaLibraryAccessTest extends KernelTestBase {
// Test that access is denied even without an entity to work with.
$state = MediaLibraryState::create('media_library.opener.field_widget', ['file', 'image'], 'file', 2, [
'entity_type_id' => 'entity_test',
'entity_type_id' => 'entity_test_with_bundle',
'bundle' => 'test',
'field_name' => $field_storage->getName(),
]);
@ -294,7 +294,7 @@ class MediaLibraryAccessTest extends KernelTestBase {
$this->assertAccess($access_result, FALSE, 'Field access denied by test module', [], ['url.query_args', 'user.permissions']);
// Assert that field access is also checked with a real entity.
$entity = EntityTest::create([
$entity = EntityTestWithBundle::create([
'type' => 'test',
'name' => $this->randomString(),
]);
@ -323,7 +323,7 @@ class MediaLibraryAccessTest extends KernelTestBase {
// Create a media library state to test access.
$state = MediaLibraryState::create('media_library.opener.field_widget', ['file', 'image'], 'file', 2, [
'entity_type_id' => 'entity_test',
'entity_type_id' => 'entity_test_with_bundle',
'bundle' => 'test',
'field_name' => 'field_test_media',
]);

View File

@ -141,7 +141,13 @@ class EntityAddUITest extends BrowserTestBase {
$this->drupalLogin($admin_user);
entity_test_create_bundle('test', 'Test label', 'entity_test_mul');
// Delete the default bundle, so that we can rely on our own.
// Delete the default bundle, so that we can rely on our own. The form
// display has to be deleted first to prevent schema errors when fields
// attached to the deleted bundle are themselves deleted, which triggers
// an update of the form display.
$this->container->get('entity_display.repository')
->getFormDisplay('entity_test_mul', 'entity_test_mul')
->delete();
entity_test_delete_bundle('entity_test_mul', 'entity_test_mul');
// One bundle exists, confirm redirection to the add-form.

View File

@ -82,6 +82,8 @@ class TermEntityReferenceTest extends KernelTestBase {
'cardinality' => 1,
]);
$field_storage->save();
entity_test_create_bundle('test_bundle');
$field = FieldConfig::create([
'field_storage' => $field_storage,
'entity_type' => 'entity_test',

View File

@ -3,7 +3,9 @@
namespace Drupal\KernelTests\Core\Entity;
use Drupal\Core\Field\Entity\BaseFieldOverride;
use Drupal\entity_test\Entity\EntityTestBundle;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
/**
* Tests validation of base_field_override entities.
@ -13,10 +15,12 @@ use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
*/
class BaseFieldOverrideValidationTest extends ConfigEntityValidationTestBase {
use ContentTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['user'];
protected static $modules = ['entity_test', 'field', 'node', 'text', 'user'];
/**
* {@inheritdoc}
@ -24,13 +28,31 @@ class BaseFieldOverrideValidationTest extends ConfigEntityValidationTestBase {
protected function setUp(): void {
parent::setUp();
$fields = $this->container->get('entity_field.manager')
->getBaseFieldDefinitions('user');
$this->installConfig('node');
$this->createContentType(['type' => 'one']);
$this->createContentType(['type' => 'another']);
$this->entity = BaseFieldOverride::createFromBaseFieldDefinition(reset($fields), 'user');
EntityTestBundle::create(['id' => 'one'])->save();
EntityTestBundle::create(['id' => 'another'])->save();
$fields = $this->container->get('entity_field.manager')
->getBaseFieldDefinitions('node');
$this->entity = BaseFieldOverride::createFromBaseFieldDefinition(reset($fields), 'one');
$this->entity->save();
}
/**
* Tests that the target bundle of the field is checked.
*/
public function testTargetBundleMustExist(): void {
$this->entity->set('bundle', 'nope');
$this->assertValidationErrors([
'' => "The 'bundle' property cannot be changed.",
'bundle' => "The 'nope' bundle does not exist on the 'node' entity type.",
]);
}
/**
* {@inheritdoc}
*/
@ -40,7 +62,10 @@ class BaseFieldOverrideValidationTest extends ConfigEntityValidationTestBase {
// settings from the *old* field_type won't match the config schema for the
// settings of the *new* field_type.
$this->entity->set('settings', []);
parent::testImmutableProperties($valid_values);
parent::testImmutableProperties([
'entity_type' => 'entity_test_with_bundle',
'bundle' => 'another',
]);
}
}

View File

@ -0,0 +1,145 @@
<?php
namespace Drupal\KernelTests\Core\Entity;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\Core\TypedData\MapDataDefinition;
use Drupal\entity_test\Entity\EntityTestBundle;
use Drupal\KernelTests\KernelTestBase;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
/**
* @group Entity
* @group Validation
*
* @covers \Drupal\Core\Validation\Plugin\Validation\Constraint\EntityBundleExistsConstraint
* @covers \Drupal\Core\Validation\Plugin\Validation\Constraint\EntityBundleExistsConstraintValidator
*/
class EntityBundleExistsConstraintValidatorTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['entity_test'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
EntityTestBundle::create([
'id' => 'foo',
'label' => 'Test',
])->save();
}
/**
* Tests that the constraint validator will only work with strings.
*/
public function testValueMustBeAString(): void {
$definition = DataDefinition::create('any')
->addConstraint('EntityBundleExists', 'entity_test_with_bundle');
$this->expectException(UnexpectedTypeException::class);
$this->expectExceptionMessage('Expected argument of type "string", "int" given');
$this->container->get('typed_data_manager')
->create($definition, 39)
->validate();
}
/**
* Tests validating a bundle of a known (static) entity type ID.
*/
public function testEntityTypeIdIsStatic(): void {
$definition = DataDefinition::create('string')
->addConstraint('EntityBundleExists', 'entity_test_with_bundle');
$violations = $this->container->get('typed_data_manager')
->create($definition, 'bar')
->validate();
$this->assertCount(1, $violations);
$this->assertSame("The 'bar' bundle does not exist on the 'entity_test_with_bundle' entity type.", (string) $violations->get(0)->getMessage());
$this->assertSame('', $violations->get(0)->getPropertyPath());
}
/**
* Tests getting the entity type ID from the parent property path.
*
* @param string $constraint_value
* The entity type ID to supply to the validation constraint. Must be a
* dynamic token starting with %.
* @param string $resolved_entity_type_id
* The actual entity type ID which should be checked for the existence of
* a bundle.
*
* @testWith ["%parent.entity_type_id", "entity_test_with_bundle"]
* ["%paren.entity_type_id", "%paren.entity_type_id"]
*/
public function testEntityTypeIdFromParent(string $constraint_value, string $resolved_entity_type_id): void {
/** @var \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data_manager */
$typed_data_manager = $this->container->get('typed_data_manager');
$this->assertStringStartsWith('%', $constraint_value);
$value_definition = DataDefinition::create('string')
->addConstraint('EntityBundleExists', $constraint_value);
$parent_definition = MapDataDefinition::create()
->setPropertyDefinition('entity_type_id', DataDefinition::create('string'))
->setPropertyDefinition('bundle', $value_definition);
$violations = $typed_data_manager->create($parent_definition, [
'entity_type_id' => 'entity_test_with_bundle',
'bundle' => 'bar',
])->validate();
$this->assertCount(1, $violations);
$this->assertSame("The 'bar' bundle does not exist on the '$resolved_entity_type_id' entity type.", (string) $violations->get(0)->getMessage());
$this->assertSame('bundle', $violations->get(0)->getPropertyPath());
}
/**
* Tests getting the entity type ID from a deeply nested property path.
*/
public function testEntityTypeIdFromMultipleParents(): void {
$tree_definition = MapDataDefinition::create()
->setPropertyDefinition('info', MapDataDefinition::create()
->setPropertyDefinition('entity_type_id', DataDefinition::create('string'))
)
->setPropertyDefinition('info2', MapDataDefinition::create()
->setPropertyDefinition('bundle', DataDefinition::create('string')
->addConstraint('EntityBundleExists', '%parent.%parent.info.entity_type_id')
)
);
$violations = $this->container->get('typed_data_manager')
->create($tree_definition, [
'info' => [
'entity_type_id' => 'entity_test_with_bundle',
],
'info2' => [
'bundle' => 'bar',
],
])
->validate();
$this->assertCount(1, $violations);
$this->assertSame("The 'bar' bundle does not exist on the 'entity_test_with_bundle' entity type.", (string) $violations->get(0)->getMessage());
$this->assertSame('info2.bundle', $violations->get(0)->getPropertyPath());
}
/**
* Tests when the constraint's entityTypeId value is not valid.
*/
public function testInvalidEntityTypeId(): void {
$entity_type_id = $this->randomMachineName();
$definition = DataDefinition::create('string')
->addConstraint('EntityBundleExists', $entity_type_id);
$violations = $this->container->get('typed_data_manager')
->create($definition, 'bar')
->validate();
$this->assertCount(1, $violations);
$this->assertSame("The 'bar' bundle does not exist on the '$entity_type_id' entity type.", (string) $violations->get(0)->getMessage());
$this->assertSame('', $violations->get(0)->getPropertyPath());
}
}

View File

@ -3,9 +3,13 @@
namespace Drupal\KernelTests\Core\Entity;
use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\Entity\EntityFormMode;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\entity_test\Entity\EntityTestBundle;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
/**
* Tests validation of entity_form_display entities.
@ -13,25 +17,41 @@ use Drupal\field\Entity\FieldStorageConfig;
* @group Entity
* @group Validation
*/
class EntityFormDisplayValidationTest extends EntityFormModeValidationTest {
class EntityFormDisplayValidationTest extends ConfigEntityValidationTestBase {
use ContentTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected bool $hasLabel = FALSE;
/**
* {@inheritdoc}
*/
protected static $modules = ['entity_test', 'field', 'node', 'text', 'user'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entity = EntityFormDisplay::create([
'targetEntityType' => 'user',
'bundle' => 'user',
// The mode was created by the parent class.
'mode' => 'test',
]);
$this->installConfig('node');
$this->createContentType(['type' => 'one']);
$this->createContentType(['type' => 'two']);
EntityTestBundle::create(['id' => 'one'])->save();
EntityTestBundle::create(['id' => 'two'])->save();
EntityFormMode::create([
'id' => 'node.test',
'label' => 'Test',
'targetEntityType' => 'node',
])->save();
$this->entity = $this->container->get(EntityDisplayRepositoryInterface::class)
->getFormDisplay('node', 'one', 'test');
$this->entity->save();
}
@ -40,7 +60,6 @@ class EntityFormDisplayValidationTest extends EntityFormModeValidationTest {
*/
public function testMultilineTextFieldWidgetPlaceholder(): void {
// First, create a field for which widget settings exist.
$this->enableModules(['field', 'text']);
$text_field_storage_config = FieldStorageConfig::create([
'type' => 'text_with_summary',
'field_name' => 'novel',
@ -76,4 +95,25 @@ class EntityFormDisplayValidationTest extends EntityFormModeValidationTest {
$this->assertValidationErrors([]);
}
/**
* Tests that the target bundle of the entity form display is checked.
*/
public function testTargetBundleMustExist(): void {
$this->entity->set('bundle', 'superhero');
$this->assertValidationErrors([
'' => "The 'bundle' property cannot be changed.",
'bundle' => "The 'superhero' bundle does not exist on the 'node' entity type.",
]);
}
/**
* {@inheritdoc}
*/
public function testImmutableProperties(array $valid_values = []): void {
parent::testImmutableProperties([
'targetEntityType' => 'entity_test_with_bundle',
'bundle' => 'two',
]);
}
}

View File

@ -96,7 +96,7 @@ class EntityQueryTest extends EntityKernelTestBase {
do {
$bundle = $this->randomMachineName();
} while ($bundles && strtolower($bundles[0]) >= strtolower($bundle));
entity_test_create_bundle($bundle);
entity_test_create_bundle($bundle, entity_type: $field_storage->getTargetEntityTypeId());
foreach ($field_storages as $field_storage) {
FieldConfig::create([
'field_storage' => $field_storage,
@ -562,6 +562,7 @@ class EntityQueryTest extends EntityKernelTestBase {
]);
$field_storage->save();
$bundle = $this->randomMachineName();
entity_test_create_bundle($bundle);
FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => $bundle,
@ -813,6 +814,7 @@ class EntityQueryTest extends EntityKernelTestBase {
*/
public function testCaseSensitivity() {
$bundle = $this->randomMachineName();
entity_test_create_bundle($bundle, entity_type: 'entity_test_mulrev');
$field_storage = FieldStorageConfig::create([
'field_name' => 'field_ci',

View File

@ -2,7 +2,11 @@
namespace Drupal\KernelTests\Core\Entity;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\Entity\EntityViewMode;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\entity_test\Entity\EntityTestBundle;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
/**
* Tests validation of entity_view_display entities.
@ -10,26 +14,63 @@ use Drupal\Core\Entity\Entity\EntityViewDisplay;
* @group Entity
* @group Validation
*/
class EntityViewDisplayValidationTest extends EntityViewModeValidationTest {
class EntityViewDisplayValidationTest extends ConfigEntityValidationTestBase {
use ContentTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected bool $hasLabel = FALSE;
/**
* {@inheritdoc}
*/
protected static $modules = ['entity_test', 'field', 'node', 'text', 'user'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entity = EntityViewDisplay::create([
'targetEntityType' => 'user',
'bundle' => 'user',
// The mode was created by the parent class.
'mode' => 'test',
]);
$this->installConfig('node');
$this->createContentType(['type' => 'one']);
$this->createContentType(['type' => 'two']);
EntityTestBundle::create(['id' => 'one'])->save();
EntityTestBundle::create(['id' => 'two'])->save();
EntityViewMode::create([
'id' => 'node.test',
'label' => 'Test',
'targetEntityType' => 'node',
])->save();
$this->entity = $this->container->get(EntityDisplayRepositoryInterface::class)
->getViewDisplay('node', 'one', 'test');
$this->entity->save();
}
/**
* Tests that the target bundle of the entity view display is checked.
*/
public function testTargetBundleMustExist(): void {
$this->entity->set('bundle', 'superhero');
$this->assertValidationErrors([
'' => "The 'bundle' property cannot be changed.",
'bundle' => "The 'superhero' bundle does not exist on the 'node' entity type.",
]);
}
/**
* {@inheritdoc}
*/
public function testImmutableProperties(array $valid_values = []): void {
parent::testImmutableProperties([
'targetEntityType' => 'entity_test_with_bundle',
'bundle' => 'two',
]);
}
}