diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml index bb4993a172..01a1eeee95 100644 --- a/core/config/schema/core.data_types.schema.yml +++ b/core/config/schema/core.data_types.schema.yml @@ -277,6 +277,10 @@ config_entity: uuid: type: string label: 'UUID' + constraints: + Length: + max: 128 + maxMessage: 'UUID: may not be longer than 128 characters.' langcode: type: string label: 'Language code' diff --git a/core/lib/Drupal/Core/Config/StorableConfigBase.php b/core/lib/Drupal/Core/Config/StorableConfigBase.php index 0751e9f59f..1c8fb2c828 100644 --- a/core/lib/Drupal/Core/Config/StorableConfigBase.php +++ b/core/lib/Drupal/Core/Config/StorableConfigBase.php @@ -129,7 +129,7 @@ public function getStorage() { * * @return \Drupal\Core\Config\Schema\Element */ - protected function getSchemaWrapper() { + public function getSchemaWrapper() { if (!isset($this->schemaWrapper)) { $definition = $this->typedConfigManager->getDefinition($this->name); $data_definition = $this->typedConfigManager->buildDataDefinition($definition, $this->data); diff --git a/core/lib/Drupal/Core/Entity/ContentEntityType.php b/core/lib/Drupal/Core/Entity/ContentEntityType.php index 0e26c3bb51..c4033b5dbb 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityType.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityType.php @@ -17,6 +17,11 @@ class ContentEntityType extends EntityType implements ContentEntityTypeInterface /** * {@inheritdoc} */ + protected $supports_validation = TRUE; + + /** + * {@inheritdoc} + */ public function __construct($definition) { parent::__construct($definition); $this->handlers += [ diff --git a/core/lib/Drupal/Core/Entity/EntityType.php b/core/lib/Drupal/Core/Entity/EntityType.php index b39fb3fa46..97cdda785f 100644 --- a/core/lib/Drupal/Core/Entity/EntityType.php +++ b/core/lib/Drupal/Core/Entity/EntityType.php @@ -278,6 +278,13 @@ class EntityType extends PluginDefinition implements EntityTypeInterface { protected $additional = []; /** + * Entity type supports validation. + * + * @var bool + */ + protected $supports_validation = FALSE; + + /** * Constructs a new EntityType. * * @param array $definition diff --git a/core/modules/config/src/Tests/ConfigEntityListTest.php b/core/modules/config/src/Tests/ConfigEntityListTest.php index e9950ea424..cb0c730b33 100644 --- a/core/modules/config/src/Tests/ConfigEntityListTest.php +++ b/core/modules/config/src/Tests/ConfigEntityListTest.php @@ -29,6 +29,8 @@ protected function setUp() { // test. \Drupal::entityManager()->getStorage('config_test')->load('override')->delete(); $this->drupalPlaceBlock('local_actions_block'); + + $this->drupalLogin($this->createUser(['view config_test', 'administer config_test'])); } /** @@ -149,7 +151,7 @@ public function testList() { */ public function testListUI() { // Log in as an administrative user to access the full menu trail. - $this->drupalLogin($this->drupalCreateUser(['access administration pages', 'administer site configuration'])); + $this->drupalLogin($this->drupalCreateUser(['access administration pages', 'administer site configuration', 'administer config_test'])); // Get the list callback page. $this->drupalGet('admin/structure/config_test'); diff --git a/core/modules/config/src/Tests/ConfigEntityTest.php b/core/modules/config/src/Tests/ConfigEntityTest.php index c0dc97bc7b..78cd31e705 100644 --- a/core/modules/config/src/Tests/ConfigEntityTest.php +++ b/core/modules/config/src/Tests/ConfigEntityTest.php @@ -231,7 +231,7 @@ public function testCRUD() { * Tests CRUD operations through the UI. */ public function testCRUDUI() { - $this->drupalLogin($this->drupalCreateUser(['administer site configuration'])); + $this->drupalLogin($this->drupalCreateUser(['administer site configuration', 'administer config_test'])); $id = strtolower($this->randomMachineName()); $label1 = $this->randomMachineName(); diff --git a/core/modules/config/tests/config_test/config_test.permissions.yml b/core/modules/config/tests/config_test/config_test.permissions.yml new file mode 100644 index 0000000000..eaf1be61b7 --- /dev/null +++ b/core/modules/config/tests/config_test/config_test.permissions.yml @@ -0,0 +1,4 @@ +view config_test: + title: 'View ConfigTest entities' +administer config_test: + title: 'Administer ConfigTest entities' diff --git a/core/modules/config/tests/config_test/src/ConfigTestAccessControlHandler.php b/core/modules/config/tests/config_test/src/ConfigTestAccessControlHandler.php index 88896f0b96..0aeb6e0d63 100644 --- a/core/modules/config/tests/config_test/src/ConfigTestAccessControlHandler.php +++ b/core/modules/config/tests/config_test/src/ConfigTestAccessControlHandler.php @@ -18,14 +18,17 @@ class ConfigTestAccessControlHandler extends EntityAccessControlHandler { * {@inheritdoc} */ public function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) { - return AccessResult::allowed(); + if ($operation === 'view') { + return AccessResult::allowedIfHasPermission($account, 'view config_test'); + } + return AccessResult::allowedIfHasPermission($account, 'administer config_test'); } /** * {@inheritdoc} */ protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) { - return AccessResult::allowed(); + return AccessResult::allowedIfHasPermission($account, 'administer config_test'); } } diff --git a/core/modules/config/tests/config_test/src/Entity/ConfigTest.php b/core/modules/config/tests/config_test/src/Entity/ConfigTest.php index 1bd1d8dee9..4f84ecfe1f 100644 --- a/core/modules/config/tests/config_test/src/Entity/ConfigTest.php +++ b/core/modules/config/tests/config_test/src/Entity/ConfigTest.php @@ -22,6 +22,7 @@ * }, * "access" = "Drupal\config_test\ConfigTestAccessControlHandler" * }, + * supports_validation = TRUE, * config_prefix = "dynamic", * entity_keys = { * "id" = "id", diff --git a/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceIntegrationTest.php b/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceIntegrationTest.php index 56484705e1..14b2cae406 100644 --- a/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceIntegrationTest.php +++ b/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceIntegrationTest.php @@ -54,7 +54,7 @@ protected function setUp() { parent::setUp(); // Create a test user. - $web_user = $this->drupalCreateUser(['administer entity_test content', 'administer entity_test fields', 'view test entity']); + $web_user = $this->drupalCreateUser(['administer entity_test content', 'administer entity_test fields', 'view test entity', 'view config_test', 'administer config_test']); $this->drupalLogin($web_user); } diff --git a/core/modules/rest/src/Plugin/Deriver/EntityDeriver.php b/core/modules/rest/src/Plugin/Deriver/EntityDeriver.php index 010fad7363..634a1bdd31 100644 --- a/core/modules/rest/src/Plugin/Deriver/EntityDeriver.php +++ b/core/modules/rest/src/Plugin/Deriver/EntityDeriver.php @@ -2,8 +2,10 @@ namespace Drupal\rest\Plugin\Deriver; +use Drupal\Core\Config\Entity\ConfigEntityTypeInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface; +use Drupal\rest\Plugin\rest\resource\ConfigEntityResource; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -88,6 +90,10 @@ public function getDerivativeDefinitions($base_plugin_definition) { } } + if ($entity_type instanceof ConfigEntityTypeInterface) { + $this->derivatives[$entity_type_id]['class'] = ConfigEntityResource::class; + } + $this->derivatives[$entity_type_id] += $base_plugin_definition; } } diff --git a/core/modules/rest/src/Plugin/rest/resource/ConfigEntityResource.php b/core/modules/rest/src/Plugin/rest/resource/ConfigEntityResource.php new file mode 100644 index 0000000000..d4b98c342f --- /dev/null +++ b/core/modules/rest/src/Plugin/rest/resource/ConfigEntityResource.php @@ -0,0 +1,27 @@ +getEditable($entity->getConfigDependencyName()); + $config->setData($entity->toArray()); + + $typed_config = $config->getSchemaWrapper(); + + $violations = $typed_config->validate(); + + $this->processViolations($violations); + } + +} diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php index a9e0ff0e86..834c2bbaf8 100644 --- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php @@ -264,36 +264,38 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity // Overwrite the received properties. $entity_keys = $entity->getEntityType()->getKeys(); - foreach ($entity->_restSubmittedFields as $field_name) { - $field = $entity->get($field_name); - - // Entity key fields need special treatment: together they uniquely - // identify the entity. Therefore it does not make sense to modify any of - // them. However, rather than throwing an error, we just ignore them as - // long as their specified values match their current values. - if (in_array($field_name, $entity_keys, TRUE)) { - // @todo Work around the wrong assumption that entity keys need special - // treatment, when only read-only fields need it. - // This will be fixed in https://www.drupal.org/node/2824851. - if ($entity->getEntityTypeId() == 'comment' && $field_name == 'status' && !$original_entity->get($field_name)->access('edit')) { - throw new AccessDeniedHttpException("Access denied on updating field '$field_name'."); + if ($entity instanceof FieldableEntityInterface) { + foreach ($entity->_restSubmittedFields as $field_name) { + $field = $entity->get($field_name); + + // Entity key fields need special treatment: together they uniquely + // identify the entity. Therefore it does not make sense to modify any of + // them. However, rather than throwing an error, we just ignore them as + // long as their specified values match their current values. + if (in_array($field_name, $entity_keys, TRUE)) { + // @todo Work around the wrong assumption that entity keys need special + // treatment, when only read-only fields need it. + // This will be fixed in https://www.drupal.org/node/2824851. + if ($entity->getEntityTypeId() == 'comment' && $field_name == 'status' && !$original_entity->get($field_name)->access('edit')) { + throw new AccessDeniedHttpException("Access denied on updating field '$field_name'."); + } + + // Unchanged values for entity keys don't need access checking. + if ($this->getCastedValueFromFieldItemList($original_entity->get($field_name)) === $this->getCastedValueFromFieldItemList($entity->get($field_name))) { + continue; + } + // It is not possible to set the language to NULL as it is automatically + // re-initialized. As it must not be empty, skip it if it is. + elseif (isset($entity_keys['langcode']) && $field_name === $entity_keys['langcode'] && $field->isEmpty()) { + continue; + } } - // Unchanged values for entity keys don't need access checking. - if ($this->getCastedValueFromFieldItemList($original_entity->get($field_name)) === $this->getCastedValueFromFieldItemList($entity->get($field_name))) { - continue; - } - // It is not possible to set the language to NULL as it is automatically - // re-initialized. As it must not be empty, skip it if it is. - elseif (isset($entity_keys['langcode']) && $field_name === $entity_keys['langcode'] && $field->isEmpty()) { - continue; + if (!$original_entity->get($field_name)->access('edit')) { + throw new AccessDeniedHttpException("Access denied on updating field '$field_name'."); } + $original_entity->set($field_name, $field->getValue()); } - - if (!$original_entity->get($field_name)->access('edit')) { - throw new AccessDeniedHttpException("Access denied on updating field '$field_name'."); - } - $original_entity->set($field_name, $field->getValue()); } // Validate the received data before saving. @@ -392,7 +394,7 @@ protected function getBaseRoute($canonical_path, $method) { */ public function availableMethods() { $methods = parent::availableMethods(); - if ($this->isConfigEntityResource()) { + if (!$this->entityType->get('supports_validation')) { // Currently only GET is supported for Config Entities. // @todo Remove when supported https://www.drupal.org/node/2300677 $unsupported_methods = ['POST', 'PUT', 'DELETE', 'PATCH']; diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php b/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php index 7bf8e824e1..4480a85bcb 100644 --- a/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php @@ -3,6 +3,7 @@ namespace Drupal\rest\Plugin\rest\resource; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\FieldableEntityInterface; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; /** @@ -22,6 +23,10 @@ * field. */ protected function checkEditFieldAccess(EntityInterface $entity) { + if (!$entity instanceof FieldableEntityInterface) { + return; + } + // Only check 'edit' permissions for fields that were actually submitted by // the user. Field access makes no difference between 'create' and 'update', // so the 'edit' operation is used here. diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php b/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php index 09b4b64bae..de1bb77f03 100644 --- a/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php @@ -33,6 +33,19 @@ protected function validate(EntityInterface $entity) { // changes. $violations->filterByFieldAccess(); + $this->processViolations($violations); + } + + /** + * Processes violations and creates a helpful exception message. + * + * @param \Drupal\Core\Entity\EntityConstraintViolationListInterface $violations + * The entity constraint violations to process. + * + * @throws \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException + * Throws a HTTP exception when the validation fails. + */ + protected function processViolations($violations) { if ($violations->count() > 0) { $message = "Unprocessable Entity: validation failed.\n"; foreach ($violations as $violation) { diff --git a/core/modules/rest/tests/modules/config_test_rest/config_test_rest.module b/core/modules/rest/tests/modules/config_test_rest/config_test_rest.module index fcd9979a11..0cffb9c1f4 100644 --- a/core/modules/rest/tests/modules/config_test_rest/config_test_rest.module +++ b/core/modules/rest/tests/modules/config_test_rest/config_test_rest.module @@ -26,5 +26,8 @@ function config_test_rest_config_test_access(EntityInterface $entity, $operation // Add permission, so that EntityResourceTestBase's scenarios can test access // being denied. By default, all access is always allowed for the config_test // config entity. - return AccessResult::forbiddenIf(!$account->hasPermission('view config_test'))->cachePerPermissions(); + if ($operation === 'view') { + return AccessResult::forbiddenIf(!$account->hasPermission('view config_test'))->cachePerPermissions(); + } + return AccessResult::neutral(); } diff --git a/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestResourceTestBase.php index 9fe073b097..b89a2718dc 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestResourceTestBase.php @@ -23,10 +23,31 @@ protected $entity; /** + * Counter used internally to have deterministic IDs. + * + * @var int + */ + protected $counter = 0; + + /** + * {@inheritdoc} + */ + protected static $firstCreatedEntityId = 'llama'; + + /** * {@inheritdoc} */ protected function setUpAuthorization($method) { - $this->grantPermissionsToTestedRole(['view config_test']); + switch ($method) { + case 'GET': + $this->grantPermissionsToTestedRole(['view config_test']); + break; + case 'POST': + case 'PATCH': + case 'DELETE': + $this->grantPermissionsToTestedRole(['administer config_test']); + break; + } } /** @@ -67,7 +88,33 @@ protected function getExpectedNormalizedEntity() { * {@inheritdoc} */ protected function getNormalizedPostEntity() { - // @todo Update in https://www.drupal.org/node/2300677. + $this->counter++; + return [ + 'id' => 'llama' . (string) $this->counter, + 'label' => 'Llamam', + ]; + } + + /** + * {@inheritdoc} + */ + protected function makeNormalizationInvalid(array $normalization) { + $normalization['label'] = ['foo', 'bar']; + return $normalization; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedUnauthorizedAccessMessage($method) { + if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) { + return parent::getExpectedUnauthorizedAccessMessage($method); + } + + if ($method === 'GET') { + return 'You are not authorized to view this config_test entity.'; + } + return "The 'administer config_test' permission is required."; } } diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php index a55ae929e9..42c42e5bb5 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php @@ -75,7 +75,7 @@ * * @var string[] */ - protected static $patchProtectedFieldNames; + protected static $patchProtectedFieldNames = []; /** * Optionally specify which field is the 'label' field. Some entities specify @@ -585,8 +585,8 @@ protected static function castToString(array $normalization) { */ public function testPost() { // @todo Remove this in https://www.drupal.org/node/2300677. - if ($this->entity instanceof ConfigEntityInterface) { - $this->assertTrue(TRUE, 'POSTing config entities is not yet supported.'); + if (!$this->entity->getEntityType()->get('supports_validation')) { + $this->assertTrue(TRUE, "This entity type doesn't support posting."); return; } @@ -598,7 +598,14 @@ public function testPost() { $parseable_valid_request_body = $this->serializer->encode($this->getNormalizedPostEntity(), static::$format); $parseable_valid_request_body_2 = $this->serializer->encode($this->getNormalizedPostEntity(), static::$format); $parseable_invalid_request_body = $this->serializer->encode($this->makeNormalizationInvalid($this->getNormalizedPostEntity()), static::$format); - $parseable_invalid_request_body_2 = $this->serializer->encode($this->getNormalizedPostEntity() + ['uuid' => [$this->randomMachineName(129)]], static::$format); + // The normalized structure is different for fieldable and non-fieldable + // entities. + if ($this->entity instanceof FieldableEntityInterface) { + $parseable_invalid_request_body_2 = $this->serializer->encode($this->getNormalizedPostEntity() + ['uuid' => [$this->randomMachineName(129)]], static::$format); + } + else { + $parseable_invalid_request_body_2 = $this->serializer->encode($this->getNormalizedPostEntity() + ['uuid' => $this->randomMachineName(129)], static::$format); + } $parseable_invalid_request_body_3 = $this->serializer->encode($this->getNormalizedPostEntity() + ['field_rest_test' => [['value' => $this->randomString()]]], static::$format); // The URL and Guzzle request options that will be used in this test. The @@ -686,10 +693,15 @@ public function testPost() { // DX: 422 when invalid entity: multiple values sent for single-value field. $response = $this->request('POST', $url, $request_options); - $label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName; - $label_field_capitalized = $this->entity->getFieldDefinition($label_field)->getLabel(); - $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n", $response); + if ($this->entity instanceof ConfigEntityInterface) { + $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nlabel: This value should be of the correct primitive type.\n", $response); + } + else { + $label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName; + $label_field_capitalized = $this->entity instanceof FieldableEntityInterface ? $this->entity->getFieldDefinition($label_field)->getLabel() : 'label'; + $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n", $response); + } $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_2; @@ -698,16 +710,23 @@ public function testPost() { // @todo Fix this in https://www.drupal.org/node/2149851. if ($this->entity->getEntityType()->hasKey('uuid')) { $response = $this->request('POST', $url, $request_options); - $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nuuid.0.value: UUID: may not be longer than 128 characters.\n", $response); + if ($this->entity instanceof FieldableEntityInterface) { + $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nuuid.0.value: UUID: may not be longer than 128 characters.\n", $response); + } + else { + $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nuuid: UUID: may not be longer than 128 characters.\n", $response); + } } - $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_3; + if ($this->entity instanceof FieldableEntityInterface) { + $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_3; - // DX: 403 when entity contains field without 'edit' access. - $response = $this->request('POST', $url, $request_options); - $this->assertResourceErrorResponse(403, "Access denied on creating field 'field_rest_test'.", $response); + // DX: 403 when entity contains field without 'edit' access. + $response = $this->request('POST', $url, $request_options); + $this->assertResourceErrorResponse(403, "Access denied on creating field 'field_rest_test'.", $response); + } $request_options[RequestOptions::BODY] = $parseable_valid_request_body; @@ -775,9 +794,8 @@ public function testPost() { * Tests a PATCH request for an entity, plus edge cases to ensure good DX. */ public function testPatch() { - // @todo Remove this in https://www.drupal.org/node/2300677. - if ($this->entity instanceof ConfigEntityInterface) { - $this->assertTrue(TRUE, 'PATCHing config entities is not yet supported.'); + if (!$this->entity->getEntityType()->get('supports_validation')) { + $this->assertTrue(TRUE, "This entity type doesn't support patching."); return; } @@ -887,18 +905,22 @@ public function testPatch() { // DX: 422 when invalid entity: multiple values sent for single-value field. - $response = $this->request('PATCH', $url, $request_options); - $label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName; - $label_field_capitalized = $this->entity->getFieldDefinition($label_field)->getLabel(); - $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n", $response); + if ($this->entity instanceof FieldableEntityInterface) { + $response = $this->request('PATCH', $url, $request_options); + $label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName; + $label_field_capitalized = $this->entity->getFieldDefinition($label_field)->getLabel(); + $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n", $response); + } $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_2; // DX: 403 when entity contains field without 'edit' access. - $response = $this->request('PATCH', $url, $request_options); - $this->assertResourceErrorResponse(403, "Access denied on updating field 'field_rest_test'.", $response); + if ($this->entity instanceof FieldableEntityInterface) { + $response = $this->request('PATCH', $url, $request_options); + $this->assertResourceErrorResponse(403, "Access denied on updating field 'field_rest_test'.", $response); + } // DX: 403 when sending PATCH request with read-only fields. @@ -941,10 +963,12 @@ public function testPatch() { $response = $this->request('PATCH', $url, $request_options); $this->assertResourceResponse(200, FALSE, $response); $this->assertFalse($response->hasHeader('X-Drupal-Cache')); - // Ensure that fields do not get deleted if they're not present in the PATCH - // request. Test this using the configurable field that we added, but which - // is not sent in the PATCH request. - $this->assertSame('All the faith he had had had had no effect on the outcome of his life.', $this->entityStorage->loadUnchanged($this->entity->id())->get('field_rest_test')->value); + if ($this->entity instanceof FieldableEntityInterface) { + // Ensure that fields do not get deleted if they're not present in the PATCH + // request. Test this using the configurable field that we added, but which + // is not sent in the PATCH request. + $this->assertSame('All the faith he had had had had no effect on the outcome of his life.', $this->entityStorage->loadUnchanged($this->entity->id())->get('field_rest_test')->value); + } $this->config('rest.settings')->set('bc_entity_resource_permissions', TRUE)->save(TRUE); @@ -970,9 +994,8 @@ public function testPatch() { * Tests a DELETE request for an entity, plus edge cases to ensure good DX. */ public function testDelete() { - // @todo Remove this in https://www.drupal.org/node/2300677. - if ($this->entity instanceof ConfigEntityInterface) { - $this->assertTrue(TRUE, 'DELETEing config entities is not yet supported.'); + if (!$this->entity->getEntityType()->get('supports_validation')) { + $this->assertTrue(TRUE, "This entity type doesn't support deletion."); return; } @@ -1145,7 +1168,16 @@ protected function getPostUrl() { protected function makeNormalizationInvalid(array $normalization) { // Add a second label to this entity to make it invalid. $label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName; - $normalization[$label_field][1]['value'] = 'Second Title'; + + if ($this->entity instanceof FieldableEntityInterface) { + $normalization[$label_field][1]['value'] = 'Second Title'; + } + else { + $normalization[$label_field] = [ + $normalization[$label_field], + 'Second title', + ]; + } return $normalization; } diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php index fb3573eec9..7145520b72 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php @@ -172,6 +172,12 @@ public function testSchemaMapping() { $expected['mapping']['name']['type'] = 'string'; $expected['mapping']['uuid']['type'] = 'string'; $expected['mapping']['uuid']['label'] = 'UUID'; + $expected['mapping']['uuid']['constraints'] = [ + 'Length' => [ + 'max' => 128, + 'maxMessage' => 'UUID: may not be longer than 128 characters.', + ], + ]; $expected['mapping']['langcode']['type'] = 'string'; $expected['mapping']['langcode']['label'] = 'Language code'; $expected['mapping']['status']['type'] = 'boolean';