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/content_moderation/src/Entity/ContentModerationState.php b/core/modules/content_moderation/src/Entity/ContentModerationState.php
index 5b1c250679..888d2ba645 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 @@
+<?php
+
+namespace Drupal\rest\Plugin\rest\resource;
+
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Specific config entity resource with special behaviour for validation.
+ */
+class ConfigEntityResource extends EntityResource {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function validate(EntityInterface $entity) {
+    // Use typed config to validate the validate the config entity.
+    /** @var \Drupal\Core\Config\TypedConfigManagerInterface $type_config_manager */
+    $type_config_manager = \Drupal::service('config.typed');
+    $typed_config = $type_config_manager->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 5d9849ded4..e0c40a93bb 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);
+    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')) {
+        // 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;
+          }
+        }
+
+        if (!$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;
-        }
+        $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,9 +394,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->get('supports_validation')) {
       $unsupported_methods = ['POST', 'PUT', 'DELETE', 'PATCH'];
       $methods = array_diff($methods, $unsupported_methods);
     }
@@ -402,16 +403,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 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..def875f456 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
@@ -5,10 +5,6 @@
  * Contains hook implementations for testing REST module.
  */
 
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Session\AccountInterface;
-
 /**
  * Implements hook_entity_type_alter().
  */
@@ -18,13 +14,3 @@ function config_test_rest_entity_type_alter(array &$entity_types) {
   // the config_test entity type, which makes REST deserialization impossible.
   unset($entity_types['config_test_no_status']);
 }
-
-/**
- * Implements hook_ENTITY_TYPE_access().
- */
-function config_test_rest_config_test_access(EntityInterface $entity, $operation, AccountInterface $account) {
-  // 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();
-}
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..5928f162c5 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,43 @@ 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.";
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function testPatch() {
+    parent::testPatch();
+    $this->entity = $this->entityStorage->loadUnchanged($this->entity->id());
+    $this->assertSame('Llamam', $this->entity->label());
+
   }
 
 }
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
index 277c47042b..84080e3a34 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -77,7 +77,7 @@
    *
    * @var string[]
    */
-  protected static $patchProtectedFieldNames;
+  protected static $patchProtectedFieldNames = [];
 
   /**
    * Optionally specify which field is the 'label' field. Some entities specify
@@ -699,9 +699,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;
     }
 
@@ -713,7 +712,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
@@ -801,9 +807,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 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;
@@ -813,16 +824,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: 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;
@@ -901,9 +919,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;
     }
 
@@ -1014,18 +1031,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.
@@ -1072,10 +1093,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);
@@ -1101,9 +1124,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;
     }
 
@@ -1277,7 +1299,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;
   }
