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 @@ -14,6 +14,11 @@ class ContentEntityType extends EntityType implements ContentEntityTypeInterface */ protected $revision_metadata_keys = []; + /** + * {@inheritdoc} + */ + protected $supports_validation = TRUE; + /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/Entity/EntityType.php b/core/lib/Drupal/Core/Entity/EntityType.php index 5f6589a6b4..1132a7a6eb 100644 --- a/core/lib/Drupal/Core/Entity/EntityType.php +++ b/core/lib/Drupal/Core/Entity/EntityType.php @@ -277,6 +277,13 @@ class EntityType extends PluginDefinition implements EntityTypeInterface { */ protected $additional = []; + /** + * Entity type supports validation. + * + * @var bool + */ + protected $supports_validation = FALSE; + /** * Constructs a new EntityType. * 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/content_moderation/src/Entity/ContentModerationState.php b/core/modules/content_moderation/src/Entity/ContentModerationState.php index e54fbbe2a8..f2d74b81ce 100644 --- a/core/modules/content_moderation/src/Entity/ContentModerationState.php +++ b/core/modules/content_moderation/src/Entity/ContentModerationState.php @@ -31,6 +31,7 @@ * data_table = "content_moderation_state_field_data", * revision_data_table = "content_moderation_state_field_revision", * translatable = TRUE, + * supports_validation = FALSE, * entity_keys = { * "id" = "id", * "revision" = "revision_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 a74e8b2a61..8940c910b6 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..91d4e89b21 --- /dev/null +++ b/core/modules/rest/src/Plugin/rest/resource/ConfigEntityResource.php @@ -0,0 +1,25 @@ +createFromNameAndData($entity->getConfigDependencyName(), $entity->toArray()); + $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 10874680a6..34c7493c08 100644 --- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php @@ -225,29 +225,31 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity throw new AccessDeniedHttpException($entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'update')); } - // Overwrite the received fields. - foreach ($entity->_restSubmittedFields as $field_name) { - $field = $entity->get($field_name); - $original_field = $original_entity->get($field_name); - - // If the user has access to view the field, we need to check update - // access regardless of the field value to avoid information disclosure. - // (Otherwise the user may try PATCHing with value after value, until they - // send the current value for the field, and then they won't get a 403 - // response anymore, which indicates that the value they sent in the PATCH - // request body matches the current value.) - if (!$original_field->access('view')) { - if (!$original_field->access('edit')) { + if ($entity instanceof FieldableEntityInterface) { + // Overwrite the received fields. + foreach ($entity->_restSubmittedFields as $field_name) { + $field = $entity->get($field_name); + $original_field = $original_entity->get($field_name); + + // If the user has access to view the field, we need to check update + // access regardless of the field value to avoid information disclosure. + // (Otherwise the user may try PATCHing with value after value, until + // they send the current value for the field, and then they won't get a + // 403 response anymore, which indicates that the value they sent in the + // PATCH request body matches the current value.) + if (!$original_field->access('view')) { + if (!$original_field->access('edit')) { + throw new AccessDeniedHttpException("Access denied on updating field '$field_name'."); + } + } + // Check access for all received fields, but only if they are being + // changed. The bundle of an entity, for example, must be provided for + // denormalization to succeed, but it may not be changed. + elseif (!$original_field->equals($field) && !$original_field->access('edit')) { throw new AccessDeniedHttpException("Access denied on updating field '$field_name'."); } + $original_entity->set($field_name, $field->getValue()); } - // Check access for all received fields, but only if they are being - // changed. The bundle of an entity, for example, must be provided for - // denormalization to succeed, but it may not be changed. - elseif (!$original_field->equals($field) && !$original_field->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. @@ -346,25 +348,14 @@ protected function getBaseRoute($canonical_path, $method) { */ public function availableMethods() { $methods = parent::availableMethods(); - if ($this->isConfigEntityResource()) { - // Currently only GET is supported for Config Entities. - // @todo Remove when supported https://www.drupal.org/node/2300677 + // Without validation, it's impossible to support creation or modification. + if (!$this->entityType->get('supports_validation')) { $unsupported_methods = ['POST', 'PUT', 'DELETE', 'PATCH']; $methods = array_diff($methods, $unsupported_methods); } return $methods; } - /** - * Checks if this resource is for a Config Entity. - * - * @return bool - * TRUE if the entity is a Config Entity, FALSE otherwise. - */ - protected function isConfigEntityResource() { - return $this->entityType instanceof ConfigEntityType; - } - /** * {@inheritdoc} */ 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..65af2c5fef 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestResourceTestBase.php @@ -3,7 +3,9 @@ namespace Drupal\Tests\rest\Functional\EntityResource\ConfigTest; use Drupal\config_test\Entity\ConfigTest; +use Drupal\Core\Url; use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase; +use GuzzleHttp\RequestOptions; abstract class ConfigTestResourceTestBase extends EntityResourceTestBase { @@ -22,11 +24,32 @@ */ protected $entity; + /** + * Counter used internally to have deterministic IDs. + * + * @var int + */ + protected $counter = 0; + + /** + * {@inheritdoc} + */ + protected static $firstCreatedEntityId = 'llama1'; + /** * {@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; + } } /** @@ -34,7 +57,7 @@ protected function setUpAuthorization($method) { */ protected function createEntity() { $config_test = ConfigTest::create([ - 'id' => 'llama', + 'id' => 'llama' . (string) $this->counter, 'label' => 'Llama', ]); $config_test->save(); @@ -48,7 +71,7 @@ protected function createEntity() { protected function getExpectedNormalizedEntity() { $normalization = [ 'uuid' => $this->entity->uuid(), - 'id' => 'llama', + 'id' => 'llama' . (string) $this->counter, 'weight' => 0, 'langcode' => 'en', 'status' => TRUE, @@ -63,11 +86,42 @@ protected function getExpectedNormalizedEntity() { return $normalization; } + /** + * {@inheritdoc} + */ + protected function assertNormalizationEdgeCases($method, Url $url, array $request_options) { + parent::assertNormalizationEdgeCases($method, $url, $request_options); + + $normalization = $this->getNormalizedPostEntity(); + $normalization['protected_property'] = 'some value'; + $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format); + $response = $this->request($method, $url, $request_options); + $this->assertResourceErrorResponse(422, '', $response); + } + /** * {@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 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 72589bd3b8..4daed7dfa1 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php @@ -6,7 +6,6 @@ use Drupal\Component\Utility\Random; use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheableResponseInterface; -use Drupal\Core\Config\Entity\ConfigEntityInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Field\Plugin\Field\FieldType\BooleanItem; @@ -82,7 +81,7 @@ * * @var string[] */ - protected static $patchProtectedFieldNames; + protected static $patchProtectedFieldNames = []; /** * Optionally specify which field is the 'label' field. Some entities specify @@ -694,9 +693,8 @@ protected static function recursiveKSort(array &$array) { * Tests a POST request for an entity, plus edge cases to ensure good DX. */ 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; } @@ -708,7 +706,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 @@ -796,9 +801,14 @@ 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 FieldableEntityInterface) { + $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); + } + else { + $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nlabel: This value should be of the correct primitive type.\n", $response); + } $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_2; @@ -808,16 +818,25 @@ 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) { + // @see \Drupal\Core\Field\Plugin\Field\FieldType\StringItem::getConstraints + $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nuuid.0.value: UUID: may not be longer than 128 characters.\n", $response); + } + else { + // @see \Symfony\Component\Validator\Constraints\Uuid + $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nuuid: This is not a valid UUID.\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; @@ -864,7 +883,7 @@ public function testPost() { foreach ($this->getNormalizedPostEntity() as $field_name => $field_normalization) { // Some top-level keys in the normalization may not be fields on the // entity (for example '_links' and '_embedded' in the HAL normalization). - if ($created_entity->hasField($field_name)) { + if ($created_entity instanceof FieldableEntityInterface && $created_entity->hasField($field_name)) { // Subset, not same, because we can e.g. send just the target_id for the // bundle in a POST request; the response will include more properties. $this->assertArraySubset(static::castToString($field_normalization), $created_entity->get($field_name)->getValue(), TRUE); @@ -915,9 +934,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; } @@ -1028,18 +1046,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); + } $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_3; @@ -1097,25 +1119,27 @@ public function testPatch() { $response = $this->request('PATCH', $url, $request_options); $this->assertResourceResponse(200, FALSE, $response); $this->assertFalse($response->hasHeader('X-Drupal-Cache')); - // Assert that the entity was indeed updated, and that the response body - // contains the serialized updated entity. - $updated_entity = $this->entityStorage->loadUnchanged($this->entity->id()); - $updated_entity_normalization = $this->serializer->normalize($updated_entity, static::$format, ['account' => $this->account]); - $this->assertSame($updated_entity_normalization, $this->serializer->decode((string) $response->getBody(), static::$format)); - // Assert that the entity was indeed created using the PATCHed values. - foreach ($this->getNormalizedPatchEntity() as $field_name => $field_normalization) { - // Some top-level keys in the normalization may not be fields on the - // entity (for example '_links' and '_embedded' in the HAL normalization). - if ($updated_entity->hasField($field_name)) { - // Subset, not same, because we can e.g. send just the target_id for the - // bundle in a PATCH request; the response will include more properties. - $this->assertArraySubset(static::castToString($field_normalization), $updated_entity->get($field_name)->getValue(), TRUE); + if ($this->entity instanceof FieldableEntityInterface) { + // Assert that the entity was indeed updated, and that the response body + // contains the serialized updated entity. + $updated_entity = $this->entityStorage->loadUnchanged($this->entity->id()); + $updated_entity_normalization = $this->serializer->normalize($updated_entity, static::$format, ['account' => $this->account]); + $this->assertSame($updated_entity_normalization, $this->serializer->decode((string) $response->getBody(), static::$format)); + // Assert that the entity was indeed created using the PATCHed values. + foreach ($this->getNormalizedPatchEntity() as $field_name => $field_normalization) { + // Some top-level keys in the normalization may not be fields on the + // entity (for example '_links' and '_embedded' in the HAL normalization). + if ($updated_entity->hasField($field_name)) { + // Subset, not same, because we can e.g. send just the target_id for the + // bundle in a PATCH request; the response will include more properties. + $this->assertArraySubset(static::castToString($field_normalization), $updated_entity->get($field_name)->getValue(), TRUE); + } } + // 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.', $updated_entity->get('field_rest_test')->value); } - // 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.', $updated_entity->get('field_rest_test')->value); $this->config('rest.settings')->set('bc_entity_resource_permissions', TRUE)->save(TRUE); @@ -1141,9 +1165,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 DELETEing."); return; } @@ -1368,7 +1391,15 @@ protected static function getModifiedEntityForPatchTesting(EntityInterface $enti 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; }