.../Comment/CommentHalJsonAnonTest.php | 2 +- .../Comment/CommentHalJsonTestBase.php | 2 +- .../EntityResource/HalEntityNormalizationTrait.php | 19 +++- .../EntityResource/Node/NodeHalJsonAnonTest.php | 2 +- .../Vocabulary/VocabularyHalJsonAnonTest.php | 3 +- .../tests/src/Functional/AnonResourceTestTrait.php | 14 +++ .../src/Functional/BasicAuthResourceTestTrait.php | 9 +- .../src/Functional/CookieResourceTestTrait.php | 13 ++- .../EntityResource/Block/BlockResourceTestBase.php | 2 +- .../EntityResource/Comment/CommentJsonAnonTest.php | 2 +- .../Comment/CommentResourceTestBase.php | 16 ++- .../ConfigTest/ConfigTestResourceTestBase.php | 5 +- .../EntityResource/EntityResourceTestBase.php | 114 +++++++++++++++------ .../EntityTest/EntityTestJsonBasicAuthTest.php | 1 - .../EntityTest/EntityTestResourceTestBase.php | 8 +- .../EntityResource/Node/NodeResourceTestBase.php | 4 +- .../EntityResource/Role/RoleResourceTestBase.php | 2 +- .../EntityResource/Term/TermResourceTestBase.php | 6 +- .../EntityResource/User/UserResourceTestBase.php | 6 +- .../Vocabulary/VocabularyResourceTestBase.php | 2 +- .../rest/tests/src/Functional/ResourceTestBase.php | 8 +- 21 files changed, 178 insertions(+), 62 deletions(-) diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonAnonTest.php index 56d94e2..ffcefc8 100644 --- a/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonAnonTest.php +++ b/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonAnonTest.php @@ -23,7 +23,7 @@ class CommentHalJsonAnonTest extends CommentHalJsonTestBase { * * @see ::setUpAuthorization */ - protected static $patchProtectedFields = [ + protected static $patchProtectedFieldNames = [ 'changed', 'thread', 'entity_type', diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonTestBase.php b/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonTestBase.php index 9ad4ead..9e42f56 100644 --- a/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonTestBase.php +++ b/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonTestBase.php @@ -40,7 +40,7 @@ * * @todo fix in https://www.drupal.org/node/2824271 */ - protected static $patchProtectedFields = [ + protected static $patchProtectedFieldNames = [ 'created', 'changed', 'status', diff --git a/core/modules/hal/tests/src/Functional/EntityResource/HalEntityNormalizationTrait.php b/core/modules/hal/tests/src/Functional/EntityResource/HalEntityNormalizationTrait.php index e3b8d9c..317110b 100644 --- a/core/modules/hal/tests/src/Functional/EntityResource/HalEntityNormalizationTrait.php +++ b/core/modules/hal/tests/src/Functional/EntityResource/HalEntityNormalizationTrait.php @@ -7,8 +7,25 @@ use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Url; +/** + * Trait for EntityResourceTestBase subclasses testing formats using HAL normalization + */ trait HalEntityNormalizationTrait { + /** + * Applies the HAL entity field normalization to an entity normalization. + * + * The HAL normalization: + * - adds a 'lang' attribute to every translatable field + * - omits reference fields, since references are stored in _links & _embedded + * - omits empty fields (fields without value) + * + * @param array $normalization + * An entity normalization. + * + * @return array + * The updated entity normalization. + */ protected function applyHalFieldNormalization(array $normalization) { if (!$this->entity instanceof FieldableEntityInterface) { throw new \LogicException('This trait should only be used for fieldable entity types.'); @@ -58,7 +75,7 @@ protected function applyHalFieldNormalization(array $normalization) { protected function removeFieldsFromNormalization(array $normalization, $field_names) { $normalization = parent::removeFieldsFromNormalization($normalization, $field_names); foreach ($field_names as $field_name) { - $relation_url = Url::fromUri('base:rest/relation/' . static::$entityType . '/' . $this->entity->bundle() . '/' . $field_name) + $relation_url = Url::fromUri('base:rest/relation/' . static::$entityTypeId . '/' . $this->entity->bundle() . '/' . $field_name) ->setAbsolute(TRUE) ->toString(); $normalization['_links'] = array_diff_key($normalization['_links'], [$relation_url => TRUE]); diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php index e755c91..72d19ae 100644 --- a/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php +++ b/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php @@ -38,7 +38,7 @@ class NodeHalJsonAnonTest extends NodeResourceTestBase { /** * {@inheritdoc} */ - protected static $patchProtectedFields = [ + protected static $patchProtectedFieldNames = [ 'created', 'changed', 'promote', diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Vocabulary/VocabularyHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/EntityResource/Vocabulary/VocabularyHalJsonAnonTest.php index fc9fed7..e4ae869 100644 --- a/core/modules/hal/tests/src/Functional/EntityResource/Vocabulary/VocabularyHalJsonAnonTest.php +++ b/core/modules/hal/tests/src/Functional/EntityResource/Vocabulary/VocabularyHalJsonAnonTest.php @@ -33,8 +33,7 @@ class VocabularyHalJsonAnonTest extends VocabularyResourceTestBase { protected static $expectedErrorMimeType = 'application/json'; /** - * Disable the GET test coverage due to bug in taxonomy module. - * @todo Fix in https://www.drupal.org/node/2805281: remove this override. + * @todo Remove this override once https://www.drupal.org/node/2805281 is fixed. */ public function testGet() { $this->markTestSkipped(); diff --git a/core/modules/rest/tests/src/Functional/AnonResourceTestTrait.php b/core/modules/rest/tests/src/Functional/AnonResourceTestTrait.php index 3e1a912..b05ddf2 100644 --- a/core/modules/rest/tests/src/Functional/AnonResourceTestTrait.php +++ b/core/modules/rest/tests/src/Functional/AnonResourceTestTrait.php @@ -5,6 +5,20 @@ use Drupal\Core\Url; use Psr\Http\Message\ResponseInterface; +/** + * Trait for ResourceTestBase subclasses testing $auth=NULL, i.e. authless/anon. + * + * Characteristics: + * - When no authentication provider is being used, there also cannot be any + * particular error response for missing authentication, since by definition + * there is not any authentication. + * - For the same reason, there are no authentication edge cases to test. + * - Because no authentication is required, this is vulnerable to CSRF attacks + * by design. Hence a REST resource should probably only allow for anonymous + * for safe (GET/HEAD) HTTP methods, and only with extreme care should unsafe + * (POST/PATCH/DELETE) HTTP methods be allowed for a REST resource that allows + * anonymous access. + */ trait AnonResourceTestTrait { /** diff --git a/core/modules/rest/tests/src/Functional/BasicAuthResourceTestTrait.php b/core/modules/rest/tests/src/Functional/BasicAuthResourceTestTrait.php index 64c4d7f..6f8c621 100644 --- a/core/modules/rest/tests/src/Functional/BasicAuthResourceTestTrait.php +++ b/core/modules/rest/tests/src/Functional/BasicAuthResourceTestTrait.php @@ -6,7 +6,14 @@ use Psr\Http\Message\ResponseInterface; /** - * ResourceTestBase::getAuthenticationRequestOptions() for basic_auth. + * Trait for ResourceTestBase subclasses testing $auth=basic_auth. + * + * Characteristics: + * - Every request must send an Authorization header. + * - When accessing a URI that requires authentication without being + * authenticated, a 401 response must be sent. + * - Because every request must send an authorization, there is no danger of + * CSRF attacks. */ trait BasicAuthResourceTestTrait { diff --git a/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php b/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php index c085247..e7ffde2 100644 --- a/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php +++ b/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php @@ -7,7 +7,18 @@ use Psr\Http\Message\ResponseInterface; /** - * ResourceTestBase::getAuthenticationRequestOptions() for cookie. + * Trait for ResourceTestBase subclasses testing $auth=cookie. + * + * Characteristics: + * - After performing a valid "log in" request, the server responds with a 2xx + * status code and a 'Set-Cookie' response header. This cookie is what + * continues to identify the user in subsequent requests. + * - When accessing a URI that requires authentication without being + * authenticated, a standard 403 response must be sent. + * - Because of the reliance on cookies, and the fact that user agents send + * cookies with every request, this is vulnerable to CSRF attacks. To mitigate + * this, the response for the "log in" request contains a CSRF token that must + * be sent with every unsafe (POST/PATCH/DELETE) HTTP request. */ trait CookieResourceTestTrait { diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockResourceTestBase.php index c569c20..d32393e 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockResourceTestBase.php @@ -15,7 +15,7 @@ /** * {@inheritdoc} */ - protected static $entityType = 'block'; + protected static $entityTypeId = 'block'; /** * @var \Drupal\block\BlockInterface diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentJsonAnonTest.php b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentJsonAnonTest.php index 10b2e31..6ce580d 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentJsonAnonTest.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentJsonAnonTest.php @@ -38,7 +38,7 @@ class CommentJsonAnonTest extends CommentResourceTestBase { * * @see ::setUpAuthorization */ - protected static $patchProtectedFields = [ + protected static $patchProtectedFieldNames = [ 'pid', 'entity_id', 'changed', diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php index 2ff0ce7..be45da4 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php @@ -22,12 +22,12 @@ /** * {@inheritdoc} */ - protected static $entityType = 'comment'; + protected static $entityTypeId = 'comment'; /** * {@inheritdoc} */ - protected static $patchProtectedFields = [ + protected static $patchProtectedFieldNames = [ 'pid', 'entity_id', 'uid', @@ -251,6 +251,18 @@ protected function getNormalizedPatchEntity() { return array_diff_key($this->getNormalizedPostEntity(), ['entity_type' => TRUE, 'entity_id' => TRUE, 'field_name' => TRUE]); } + /** + * Tests POSTing a comment without critical base fields. + * + * testPost() is testing with the most minimal normalization possible: the one + * returned by ::getNormalizedPostEntity(). + * + * But Comment entities have some very special edge cases: + * - base fields that are not marked as required in \Drupal\comment\Entity\Comment::baseFieldDefinitions() + * yet in fact are required. + * - base fields that are marked as required, but yet can still result in + * validation errors other than "missing required field". + */ public function testPostDxWithoutCriticalBaseFields() { $this->initAuthentication(); $this->provisionEntityResource(); 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 50c6ec9..600a254 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestResourceTestBase.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\rest\Functional\EntityResource\ConfigTest; +use Drupal\config_test\Entity\ConfigTest; use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase; abstract class ConfigTestResourceTestBase extends EntityResourceTestBase { @@ -14,7 +15,7 @@ /** * {@inheritdoc} */ - protected static $entityType = 'config_test'; + protected static $entityTypeId = 'config_test'; /** * @var \Drupal\config_test\ConfigTestInterface @@ -32,7 +33,7 @@ protected function setUpAuthorization($method) { * {@inheritdoc} */ protected function createEntity() { - $config_test = entity_create('config_test', [ + $config_test = ConfigTest::create([ 'id' => 'llama', 'label' => 'Llama', ]); diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php index 1906020..f091331 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php @@ -23,6 +23,43 @@ * * Subclass this for every entity type. Also respect instructions in * \Drupal\rest\Tests\ResourceTestBase. + * + * For example, for the node test coverage, there is the (abstract) + * \Drupal\Tests\rest\Functional\EntityResource\Node\NodeResourceTestBase, which + * is then again subclassed for every authentication provider: + * - \Drupal\Tests\rest\Functional\EntityResource\Node\NodeJsonAnonTest + * - \Drupal\Tests\rest\Functional\EntityResource\Node\NodeJsonBasicAuthTest + * - \Drupal\Tests\rest\Functional\EntityResource\Node\NodeJsonCookieTest + * But the HAL module also adds a new format ('hal_json'), so that format also + * needs test coverage (for its own peculiarities in normalization & encoding): + * - \Drupal\Tests\hal\Functional\EntityResource\Node\NodeHalJsonAnonTest + * - \Drupal\Tests\hal\Functional\EntityResource\Node\NodeHalJsonBasicAuthTest + * - \Drupal\Tests\hal\Functional\EntityResource\Node\NodeHalJsonCookieTest + * + * In other words: for every entity type there should be: + * 1. an abstract subclass that includes the entity type-specific authorization + * (permissions or perhaps custom access control handling, such as node + * grants), plus + * 2. a concrete subclass extending the abstract entity type-specific subclass + * that specifies the exact @code $format @endcode, @code $mimeType @endcode, + * @code $expectedErrorMimeType @endcode and @code $auth @endcode for this + * concrete test. Usually that's all that's necessary: most concrete + * subclasses will be very thin. + * + * For every of these concrete subclasses, a comprehensive test scenario will + * run per HTTP method: + * - ::testGet() + * - ::testPost() + * - ::testPatch() + * - ::testDelete() + * + * If there is an entity type-specific edge case scenario to test, then add that + * to the entity type-specific abstract subclass. Example: + * \Drupal\Tests\rest\Functional\EntityResource\Comment\CommentResourceTestBase::testPostDxWithoutCriticalBaseFields + * + * If there is an entity type-specific format-specific edge case to test, then + * add that to a concrete subclass. Example: + * \Drupal\Tests\hal\Functional\EntityResource\Comment\CommentHalJsonTestBase::$patchProtectedFieldNames */ abstract class EntityResourceTestBase extends ResourceTestBase { @@ -31,14 +68,14 @@ * * @var string */ - protected static $entityType = NULL; + protected static $entityTypeId = NULL; /** * The fields that are protected against modification during PATCH requests. * * @var string[] */ - protected static $patchProtectedFields; + protected static $patchProtectedFieldNames; /** * Optionally specify which field is the 'label' field. Some entities specify @@ -48,7 +85,7 @@ * * @var string|null */ - protected static $labelField = NULL; + protected static $labelFieldName = NULL; /** * The entity ID for the first created entity in testPost(). @@ -105,7 +142,7 @@ protected function provisionEntityResource() { // It's possible to not have any authentication providers enabled, when // testing public (anonymous) usage of a REST resource. $auth = isset(static::$auth) ? [static::$auth] : []; - $this->provisionResource('entity.' . static::$entityType, [static::$format], $auth); + $this->provisionResource('entity.' . static::$entityTypeId, [static::$format], $auth); } /** @@ -116,7 +153,7 @@ public function setUp() { $this->serializer = $this->container->get('serializer'); $this->entityStorage = $this->container->get('entity_type.manager') - ->getStorage(static::$entityType); + ->getStorage(static::$entityTypeId); // Set up a HTTP client that accepts relative URLs. $this->httpClient = $this->container->get('http_client_factory') @@ -128,14 +165,14 @@ public function setUp() { if ($this->entity instanceof FieldableEntityInterface) { // Add access-protected field. FieldStorageConfig::create([ - 'entity_type' => static::$entityType, + 'entity_type' => static::$entityTypeId, 'field_name' => 'field_rest_test', 'type' => 'text', ]) ->setCardinality(1) ->save(); FieldConfig::create([ - 'entity_type' => static::$entityType, + 'entity_type' => static::$entityTypeId, 'field_name' => 'field_rest_test', 'bundle' => $this->entity->bundle(), ]) @@ -202,6 +239,9 @@ protected function getNormalizedPatchEntity() { return $this->getNormalizedPostEntity(); } + /** + * Tests GETting an entity, plus edge cases to ensure good DX. + */ public function testGet() { $this->initAuthentication(); $has_canonical_url = $this->entity->hasLinkTemplate('canonical'); @@ -325,7 +365,7 @@ public function testGet() { $this->assertResourceErrorResponse(403, '', $response); - $this->grantPermissionsToTestedRole(['restful get entity:' . static::$entityType]); + $this->grantPermissionsToTestedRole(['restful get entity:' . static::$entityTypeId]); // 200 for well-formed request. @@ -352,29 +392,21 @@ public function testGet() { $this->assertSame([static::$expectedErrorMimeType], $response->getHeader('Content-Type')); - $url = Url::fromRoute('rest.entity.' . static::$entityType . '.GET.' . static::$format); - $url->setRouteParameter(static::$entityType, 987654321); + $url = Url::fromRoute('rest.entity.' . static::$entityTypeId . '.GET.' . static::$format); + $url->setRouteParameter(static::$entityTypeId, 987654321); $url->setOption('query', ['_format' => static::$format]); // DX: 404 when GETting non-existing entity. $response = $this->request('GET', $url, $request_options); - $path = str_replace('987654321', '{' . static::$entityType . '}', $url->setAbsolute()->setOptions(['base_url' => '', 'query' => []])->toString()); - $message = 'The "' . static::$entityType . '" parameter was not converted for the path "' . $path . '" (route name: "rest.entity.' . static::$entityType . '.GET.' . static::$format . '")'; + $path = str_replace('987654321', '{' . static::$entityTypeId . '}', $url->setAbsolute()->setOptions(['base_url' => '', 'query' => []])->toString()); + $message = 'The "' . static::$entityTypeId . '" parameter was not converted for the path "' . $path . '" (route name: "rest.entity.' . static::$entityTypeId . '.GET.' . static::$format . '")'; $this->assertResourceErrorResponse(404, $message, $response); } /** - * Gets an entity resource's GET/PATCH/DELETE URL. - * - * @return \Drupal\Core\Url - * The URL to GET/PATCH/DELETE. + * Tests POSTing an entity, plus edge cases to ensure good DX. */ - protected function getUrl() { - $has_canonical_url = $this->entity->hasLinkTemplate('canonical'); - return $has_canonical_url ? $this->entity->toUrl() : Url::fromUri('base:entity/' . static::$entityType . '/' . $this->entity->id()); - } - public function testPost() { // @todo Remove this in https://www.drupal.org/node/2300677. if ($this->entity instanceof ConfigEntityInterface) { @@ -494,7 +526,7 @@ 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::$labelField; + $label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName; $label_field_capitalized = ucfirst($label_field); // @todo use this commented line instead of the 3 lines thereafter once https://www.drupal.org/node/2813755 lands. // $this->assertErrorResponse(422, "Unprocessable Entity: validation failed.\ntitle: Title: this field cannot hold more than 1 values.\n", $response); @@ -548,7 +580,7 @@ public function testPost() { $this->assertResourceErrorResponse(403, '', $response); - $this->grantPermissionsToTestedRole(['restful post entity:' . static::$entityType]); + $this->grantPermissionsToTestedRole(['restful post entity:' . static::$entityTypeId]); // 201 for well-formed request. @@ -557,6 +589,9 @@ public function testPost() { $this->assertSame([str_replace($this->entity->id(), static::$secondCreatedEntityId, $this->entity->toUrl('canonical')->setAbsolute(TRUE)->toString())], $response->getHeader('Location')); } + /** + * Tests PATCHing 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) { @@ -674,7 +709,7 @@ 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::$labelField; + $label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName; $label_field_capitalized = ucfirst($label_field); // @todo use this commented line instead of the 3 lines thereafter once https://www.drupal.org/node/2813755 lands. // $this->assertErrorResponse(422, "Unprocessable Entity: validation failed.\ntitle: Title: this field cannot hold more than 1 values.\n", $response); @@ -697,15 +732,15 @@ public function testPatch() { // 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::$patchProtectedFields); $i++) { - $max_normalization = $this->removeFieldsFromNormalization($max_normalization, array_slice(static::$patchProtectedFields, 0, $i)); + 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::$patchProtectedFields[$i] . "'.", $response); + $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::$patchProtectedFields); + $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); @@ -739,7 +774,7 @@ public function testPatch() { $this->assertResourceErrorResponse(403, '', $response); - $this->grantPermissionsToTestedRole(['restful patch entity:' . static::$entityType]); + $this->grantPermissionsToTestedRole(['restful patch entity:' . static::$entityTypeId]); // 200 for well-formed request. @@ -747,6 +782,9 @@ public function testPatch() { $this->assertResourceResponse(200, FALSE, $response); } + /** + * Tests DELETEing 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) { @@ -834,7 +872,7 @@ public function testDelete() { $this->assertResourceErrorResponse(403, '', $response); - $this->grantPermissionsToTestedRole(['restful delete entity:' . static::$entityType]); + $this->grantPermissionsToTestedRole(['restful delete entity:' . static::$entityTypeId]); // 204 for well-formed request. @@ -845,6 +883,18 @@ public function testDelete() { $this->assertSame('', $response->getBody()->getContents()); } + + /** + * Gets an entity resource's GET/PATCH/DELETE URL. + * + * @return \Drupal\Core\Url + * The URL to GET/PATCH/DELETE. + */ + protected function getUrl() { + $has_canonical_url = $this->entity->hasLinkTemplate('canonical'); + return $has_canonical_url ? $this->entity->toUrl() : Url::fromUri('base:entity/' . static::$entityTypeId . '/' . $this->entity->id()); + } + /** * Gets an entity resource's POST URL. * @@ -853,7 +903,7 @@ public function testDelete() { */ protected function getPostUrl() { $has_canonical_url = $this->entity->hasLinkTemplate('https://www.drupal.org/link-relations/create'); - return $has_canonical_url ? $this->entity->toUrl() : Url::fromUri('base:entity/' . static::$entityType); + return $has_canonical_url ? $this->entity->toUrl() : Url::fromUri('base:entity/' . static::$entityTypeId); } /** @@ -867,7 +917,7 @@ protected function getPostUrl() { */ protected function makeNormalizationInvalid(array $normalization) { // Add a second label to this entity to make it invalid. - $label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelField; + $label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName; $normalization[$label_field][1]['value'] = 'Second Title'; return $normalization; diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestJsonBasicAuthTest.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestJsonBasicAuthTest.php index 4625ed3..be75784 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestJsonBasicAuthTest.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestJsonBasicAuthTest.php @@ -42,5 +42,4 @@ class EntityTestJsonBasicAuthTest extends EntityTestResourceTestBase { JsonBasicAuthWorkaroundFor2805281Trait::assertResponseWhenMissingAuthentication insteadof BasicAuthResourceTestTrait; } - } diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php index 437d9eb..da86d00 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php @@ -16,12 +16,12 @@ /** * {@inheritdoc} */ - protected static $entityType = 'entity_test'; + protected static $entityTypeId = 'entity_test'; /** * {@inheritdoc} */ - protected static $patchProtectedFields = []; + protected static $patchProtectedFieldNames = []; /** * @var \Drupal\entity_test\Entity\EntityTest @@ -50,10 +50,10 @@ protected function setUpAuthorization($method) { * {@inheritdoc} */ protected function createEntity() { - $entity_test = EntityTest::create(array( + $entity_test = EntityTest::create([ 'name' => 'Llama', 'type' => 'entity_test', - )); + ]); $entity_test->setOwnerId(0); $entity_test->save(); diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php index bb8b495..d7651c4 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php @@ -17,12 +17,12 @@ /** * {@inheritdoc} */ - protected static $entityType = 'node'; + protected static $entityTypeId = 'node'; /** * {@inheritdoc} */ - protected static $patchProtectedFields = [ + protected static $patchProtectedFieldNames = [ 'uid', 'created', 'changed', diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Role/RoleResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Role/RoleResourceTestBase.php index 4cc247c..5ea3154 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Role/RoleResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Role/RoleResourceTestBase.php @@ -16,7 +16,7 @@ /** * {@inheritdoc} */ - protected static $entityType = 'user_role'; + protected static $entityTypeId = 'user_role'; /** * @var \Drupal\user\RoleInterface diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php index d8bc693..b6dce4f 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php @@ -16,12 +16,12 @@ /** * {@inheritdoc} */ - protected static $entityType = 'taxonomy_term'; + protected static $entityTypeId = 'taxonomy_term'; /** * {@inheritdoc} */ - protected static $patchProtectedFields = [ + protected static $patchProtectedFieldNames = [ 'changed', ]; @@ -41,7 +41,7 @@ protected function setUpAuthorization($method) { case 'POST': case 'PATCH': case 'DELETE': - // @todo Create issue similar to https://www.drupal.org/node/2808217. + // @todo Update once https://www.drupal.org/node/2824408 lands. $this->grantPermissionsToTestedRole(['administer taxonomy']); break; } diff --git a/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php index 5630fd5..195e1ad 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php @@ -17,12 +17,12 @@ /** * {@inheritdoc} */ - protected static $entityType = 'user'; + protected static $entityTypeId = 'user'; /** * {@inheritdoc} */ - protected static $patchProtectedFields = [ + protected static $patchProtectedFieldNames = [ 'changed', ]; @@ -34,7 +34,7 @@ /** * {@inheritdoc} */ - protected static $labelField = 'name'; + protected static $labelFieldName = 'name'; /** * {@inheritdoc} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Vocabulary/VocabularyResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Vocabulary/VocabularyResourceTestBase.php index e812fbb..97dcb79 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Vocabulary/VocabularyResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Vocabulary/VocabularyResourceTestBase.php @@ -18,7 +18,7 @@ /** * {@inheritdoc} */ - protected static $entityType = 'taxonomy_vocabulary'; + protected static $entityTypeId = 'taxonomy_vocabulary'; /** * @var \Drupal\taxonomy\VocabularyInterface diff --git a/core/modules/rest/tests/src/Functional/ResourceTestBase.php b/core/modules/rest/tests/src/Functional/ResourceTestBase.php index 549ebad..191b9e2 100644 --- a/core/modules/rest/tests/src/Functional/ResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/ResourceTestBase.php @@ -13,7 +13,13 @@ use Psr\Http\Message\ResponseInterface; /** - * Subclass this for every REST resource, every format and every auth mechanism. + * Subclass this for every REST resource, every format and every auth provider. + * + * For more guidance see \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase + * which has recommendations for testing the \Drupal\rest\Plugin\rest\resource\EntityResource + * REST resource for every format and every auth provider. It's a special case + * (because that single REST resource generates supports not just one thing, but + * many things — multiple entity types), but the same principles apply. */ abstract class ResourceTestBase extends BrowserTestBase {