diff --git a/core/lib/Drupal/Core/Entity/ConfigValidatableTrait.php b/core/lib/Drupal/Core/Entity/ConfigValidatableTrait.php new file mode 100644 index 0000000..fc9db30 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/ConfigValidatableTrait.php @@ -0,0 +1,43 @@ +createFromNameAndData($this->getConfigDependencyName(), $this->toArray()); + return $typed_config->validate(); + } + + /** + * Implements validate for \Drupal\Core\Entity\ValidatableInterface. + */ + public function isValidationRequired() { + return $this->validationIsRequired; + + } + + /** + * Implements validate for \Drupal\Core\Entity\ValidatableInterface. + */ + public function setValidationRequired($required) { + $this->validationIsRequired = $required; + } + +} diff --git a/core/lib/Drupal/Core/Entity/FieldableEntityInterface.php b/core/lib/Drupal/Core/Entity/FieldableEntityInterface.php index d21a2a8..4fa05b6 100644 --- a/core/lib/Drupal/Core/Entity/FieldableEntityInterface.php +++ b/core/lib/Drupal/Core/Entity/FieldableEntityInterface.php @@ -20,7 +20,7 @@ * * @ingroup entity_api */ -interface FieldableEntityInterface extends EntityInterface { +interface FieldableEntityInterface extends EntityInterface, ValidatableInterface { /** * Provides base field definitions for an entity type. @@ -209,31 +209,4 @@ public function getTranslatableFields($include_computed = TRUE); */ public function onChange($field_name); - /** - * Validates the currently set values. - * - * @return \Drupal\Core\Entity\EntityConstraintViolationListInterface - * A list of constraint violations. If the list is empty, validation - * succeeded. - */ - public function validate(); - - /** - * Checks whether entity validation is required before saving the entity. - * - * @return bool - * TRUE if validation is required, FALSE if not. - */ - public function isValidationRequired(); - - /** - * Sets whether entity validation is required before saving the entity. - * - * @param bool $required - * TRUE if validation is required, FALSE otherwise. - * - * @return $this - */ - public function setValidationRequired($required); - } diff --git a/core/lib/Drupal/Core/Entity/ValidatableInterface.php b/core/lib/Drupal/Core/Entity/ValidatableInterface.php new file mode 100644 index 0000000..3fded8b --- /dev/null +++ b/core/lib/Drupal/Core/Entity/ValidatableInterface.php @@ -0,0 +1,39 @@ +getStorage('config_test')->load('override')->delete(); $this->drupalPlaceBlock('local_actions_block'); + + $this->drupalLogin($this->createUser(['view config_test', 'administer config_test'])); } /** @@ -152,7 +154,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 a0c857b..2738918 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/schema/config_test.schema.yml b/core/modules/config/tests/config_test/config/schema/config_test.schema.yml index c25a577..d01ede9 100644 --- a/core/modules/config/tests/config_test/config/schema/config_test.schema.yml +++ b/core/modules/config/tests/config_test/config/schema/config_test.schema.yml @@ -24,6 +24,8 @@ config_test_dynamic: protected_property: type: string label: 'Protected property' + constraints: + ConfigTestProtectedProperty: {} config_test.dynamic.*: type: config_test_dynamic 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 0000000..eaf1be6 --- /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 88896f0..0aeb6e0 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/ConfigTestInterface.php b/core/modules/config/tests/config_test/src/ConfigTestInterface.php index e2cf7f4..3a8c29f 100644 --- a/core/modules/config/tests/config_test/src/ConfigTestInterface.php +++ b/core/modules/config/tests/config_test/src/ConfigTestInterface.php @@ -3,10 +3,11 @@ namespace Drupal\config_test; use Drupal\Core\Config\Entity\ConfigEntityInterface; +use Drupal\Core\Entity\ValidatableInterface; /** * Provides an interface defining a config_test entity. */ -interface ConfigTestInterface extends ConfigEntityInterface { +interface ConfigTestInterface extends ConfigEntityInterface, ValidatableInterface { } 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 1bd1d8d..960ca2b 100644 --- a/core/modules/config/tests/config_test/src/Entity/ConfigTest.php +++ b/core/modules/config/tests/config_test/src/Entity/ConfigTest.php @@ -5,6 +5,7 @@ use Drupal\Core\Config\Entity\ConfigEntityBase; use Drupal\config_test\ConfigTestInterface; use Drupal\Core\Config\Entity\ConfigEntityInterface; +use Drupal\Core\Entity\ConfigValidatableTrait; use Drupal\Core\Entity\EntityStorageInterface; /** @@ -39,6 +40,8 @@ */ class ConfigTest extends ConfigEntityBase implements ConfigTestInterface { + use ConfigValidatableTrait; + /** * The machine name for the configuration entity. * diff --git a/core/modules/config/tests/config_test/src/Plugin/Validation/Constraint/ConfigTestProtectedProperty.php b/core/modules/config/tests/config_test/src/Plugin/Validation/Constraint/ConfigTestProtectedProperty.php new file mode 100644 index 0000000..0c1e531 --- /dev/null +++ b/core/modules/config/tests/config_test/src/Plugin/Validation/Constraint/ConfigTestProtectedProperty.php @@ -0,0 +1,19 @@ +context->addViolation($constraint->message); + } + } + +} diff --git a/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceIntegrationTest.php b/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceIntegrationTest.php index 5648470..14b2cae 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/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php index 5d9849d..789b3a1 100644 --- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php @@ -5,12 +5,13 @@ use Drupal\Component\Plugin\DependentPluginInterface; use Drupal\Component\Plugin\PluginManagerInterface; use Drupal\Core\Cache\CacheableResponseInterface; -use Drupal\Core\Config\Entity\ConfigEntityType; +use Drupal\Core\Config\Entity\ConfigEntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityStorageException; +use Drupal\Core\Entity\ValidatableInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\TypedData\PrimitiveInterface; use Drupal\rest\Plugin\ResourceBase; @@ -263,39 +264,47 @@ 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) { + $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'."); + } + + // 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'."); + } + elseif ($entity instanceof ConfigEntityInterface && $original_entity instanceof ConfigEntityInterface) { + foreach ($entity->_restSubmittedFields as $field_name) { + $original_entity->set($field_name, $entity->get($field_name)); } - $original_entity->set($field_name, $field->getValue()); } + // Validate the received data before saving. $this->validate($original_entity); try { @@ -392,9 +401,8 @@ 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->entityClassImplements(ValidatableInterface::class)) { $unsupported_methods = ['POST', 'PUT', 'DELETE', 'PATCH']; $methods = array_diff($methods, $unsupported_methods); } @@ -402,16 +410,6 @@ public function availableMethods() { } /** - * 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} */ public function calculateDependencies() { diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php b/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php index 7bf8e82..4480a85 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 09b4b64..d3ee622 100644 --- a/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php @@ -3,8 +3,9 @@ namespace Drupal\rest\Plugin\rest\resource; use Drupal\Component\Render\PlainTextOutput; +use Drupal\Core\Entity\EntityConstraintViolationListInterface; use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Entity\ValidatableInterface; use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; /** @@ -24,15 +25,31 @@ */ protected function validate(EntityInterface $entity) { // @todo Remove when https://www.drupal.org/node/2164373 is committed. - if (!$entity instanceof FieldableEntityInterface) { + if (!$entity instanceof ValidatableInterface) { return; } $violations = $entity->validate(); - // Remove violations of inaccessible fields as they cannot stem from our - // changes. - $violations->filterByFieldAccess(); + if ($violations instanceof EntityConstraintViolationListInterface) { + // Remove violations of inaccessible fields as they cannot stem from our + // 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 fcd9979..0cffb9c 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 9fe073b..f49fbc9 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 { @@ -23,10 +25,31 @@ 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, @@ -66,8 +89,49 @@ protected function getExpectedNormalizedEntity() { /** * {@inheritdoc} */ - protected function getNormalizedPostEntity() { - // @todo Update in https://www.drupal.org/node/2300677. + 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, "Unprocessable Entity: validation failed.\nprotected_property: Protected property cannot be changed.\n", $response); + } + + /** + * {@inheritdoc} + */ + protected function getNormalizedPostEntity($count_up = TRUE) { + if ($count_up) { + $this->counter++; + } + + return [ + 'id' => 'llama' . (string) $this->counter, + 'label' => 'Llamam', + ]; + } + + /** + * {@inheritdoc} + */ + protected function getNormalizedPatchEntity() { + return $this->getNormalizedPostEntity(FALSE); + } + + /** + * {@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 fe4ebb8..d552e36 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php @@ -5,8 +5,8 @@ use Drupal\Component\Utility\NestedArray; use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheableResponseInterface; -use Drupal\Core\Config\Entity\ConfigEntityInterface; use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Entity\ValidatableInterface; use Drupal\Core\Url; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; @@ -77,7 +77,7 @@ * * @var string[] */ - protected static $patchProtectedFieldNames; + protected static $patchProtectedFieldNames = []; /** * Optionally specify which field is the 'label' field. Some entities specify @@ -689,9 +689,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()->entityClassImplements(ValidatableInterface::class)) { + $this->assertTrue(TRUE, "This entity type doesn't support POSTing."); return; } @@ -703,7 +702,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 @@ -791,9 +797,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; @@ -803,16 +814,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; @@ -859,7 +879,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); @@ -910,9 +930,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()->entityClassImplements(ValidatableInterface::class)) { + $this->assertTrue(TRUE, "This entity type doesn't support PATCHing."); return; } @@ -1024,37 +1043,42 @@ 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) { + $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.\n$label_field: This value should be of the correct primitive type.\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); - - - // DX: 403 when sending PATCH request with read-only fields. - // First send all fields (the "maximum normalization"). Assert the expected - // error message for the first PATCH-protected field. Remove that field from - // the normalization, send another request, assert the next PATCH-protected - // field error message. And so on. - $max_normalization = $this->getNormalizedPatchEntity() + $this->serializer->normalize($this->entity, static::$format); - for ($i = 0; $i < count(static::$patchProtectedFieldNames); $i++) { - $max_normalization = $this->removeFieldsFromNormalization($max_normalization, array_slice(static::$patchProtectedFieldNames, 0, $i)); + 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. + // First send all fields (the "maximum normalization"). Assert the expected + // error message for the first PATCH-protected field. Remove that field from + // the normalization, send another request, assert the next PATCH-protected + // field error message. And so on. + $max_normalization = $this->getNormalizedPatchEntity() + $this->serializer->normalize($this->entity, static::$format); + for ($i = 0; $i < count(static::$patchProtectedFieldNames); $i++) { + $max_normalization = $this->removeFieldsFromNormalization($max_normalization, array_slice(static::$patchProtectedFieldNames, 0, $i)); + $request_options[RequestOptions::BODY] = $this->serializer->serialize($max_normalization, static::$format); + $response = $this->request('PATCH', $url, $request_options); + $this->assertResourceErrorResponse(403, "Access denied on updating field '" . static::$patchProtectedFieldNames[$i] . "'.", $response); + } + // 200 for well-formed request that sends the maximum number of fields. + $max_normalization = $this->removeFieldsFromNormalization($max_normalization, static::$patchProtectedFieldNames); $request_options[RequestOptions::BODY] = $this->serializer->serialize($max_normalization, static::$format); $response = $this->request('PATCH', $url, $request_options); - $this->assertResourceErrorResponse(403, "Access denied on updating field '" . static::$patchProtectedFieldNames[$i] . "'.", $response); + $this->assertResourceResponse(200, FALSE, $response); } - // 200 for well-formed request that sends the maximum number of fields. - $max_normalization = $this->removeFieldsFromNormalization($max_normalization, static::$patchProtectedFieldNames); - $request_options[RequestOptions::BODY] = $this->serializer->serialize($max_normalization, static::$format); - $response = $this->request('PATCH', $url, $request_options); - $this->assertResourceResponse(200, FALSE, $response); - $request_options[RequestOptions::BODY] = $parseable_valid_request_body; @@ -1080,25 +1104,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); @@ -1124,9 +1150,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()->entityClassImplements(ValidatableInterface::class)) { + $this->assertTrue(TRUE, "This entity type doesn't support DELETEing."); return; } @@ -1303,7 +1328,15 @@ protected function getEntityResourcePostUrl() { 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; }