.../rest/tests/modules/rest_test/rest_test.module | 5 +- .../EntityResource/Block/BlockResourceTestBase.php | 3 + .../Comment/CommentResourceTestBase.php | 10 ++ .../EntityResource/EntityResourceTestBase.php | 194 ++++++++++++++++++++- .../EntityTest/EntityTestResourceTestBase.php | 3 + .../EntityResource/Node/NodeResourceTestBase.php | 3 + .../EntityResource/Term/TermResourceTestBase.php | 4 + .../EntityResource/User/UserResourceTestBase.php | 3 + 8 files changed, 216 insertions(+), 9 deletions(-) diff --git a/core/modules/rest/tests/modules/rest_test/rest_test.module b/core/modules/rest/tests/modules/rest_test/rest_test.module index 946d401..78600b9 100644 --- a/core/modules/rest/tests/modules/rest_test/rest_test.module +++ b/core/modules/rest/tests/modules/rest_test/rest_test.module @@ -36,10 +36,7 @@ function rest_test_entity_field_access($operation, FieldDefinitionInterface $fie // Never ever allow this field to be viewed: this lets EntityResourceTestBase::testGet() test in a "vanilla" way. return AccessResult::forbidden(); case 'edit': - if ($items && $items->value === 'no access value') { - return AccessResult::forbidden(); - } - break; + return AccessResult::forbidden(); } } 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 e1f9606..268f690 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockResourceTestBase.php @@ -28,6 +28,9 @@ protected function setUpAuthorization($method) { case 'POST': $this->grantPermissionsToTestedRole(['administer blocks']); break; + case 'PATCH': + $this->grantPermissionsToTestedRole(['administer blocks']); + break; } } 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 d03c817..871d5b7 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php @@ -35,6 +35,9 @@ protected function setUpAuthorization($method) { case 'POST': $this->grantPermissionsToTestedRole(['post comments']); break; + case 'PATCH': + $this->grantPermissionsToTestedRole(['administer comments']); + break; } } @@ -207,6 +210,13 @@ protected function getNormalizedPostEntity() { ]; } + /** + * {@inheritdoc} + */ + protected function getNormalizedPatchEntity() { + return array_diff_key($this->getNormalizedPostEntity(), ['entity_type' => TRUE, 'entity_id' => TRUE, 'field_name' => TRUE]); + } + public function testPostDxWithoutCriticalBaseFields() { $this->initAuthentication(); $this->provisionEntityResource(); diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php index a86495b..e984d7f 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php @@ -157,6 +157,20 @@ public function setUp() { */ abstract protected function getNormalizedPostEntity(); + /** + * Returns the normalized PATCH entity. + * + * By default, reuses ::getNormalizedPostEntity(), which works fine for most + * entity types. A counterexample: the 'comment' entity type. + * + * @see ::testPatch + * + * @return array + */ + protected function getNormalizedPatchEntity() { + return $this->getNormalizedPostEntity(); + } + public function testGet() { $this->initAuthentication(); $has_canonical_url = $this->entity->hasLinkTemplate('canonical'); @@ -344,7 +358,7 @@ public function testPost() { $unparseable_request_body = '!{>}<'; $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->getInvalidNormalizedEntityToCreate(), static::$format); + $parseable_invalid_request_body = $this->serializer->encode($this->makeNormalizationInvalid($this->getNormalizedPostEntity()), static::$format); // @todo Change to ['uuid' => UUID] when https://www.drupal.org/node/2820743 lands. $parseable_invalid_request_body_2 = $this->serializer->encode($this->getNormalizedPostEntity() + ['uuid' => [['value' => $this->randomMachineName(129)]]], static::$format); $parseable_invalid_request_body_3 = $this->serializer->encode($this->getNormalizedPostEntity() + ['field_rest_test' => [['value' => 'no access value']]], static::$format); @@ -475,6 +489,7 @@ public function testPost() { // DX: 403 when entity contains field without 'edit' access. $response = $this->request('POST', $url, $request_options); + // @todo Add trailing period once https://www.drupal.org/node/2821013 is fixed. $this->assertResourceErrorResponse(403, "Access denied on creating field 'field_rest_test'", $response); @@ -511,6 +526,171 @@ public function testPost() { $this->assertSame([str_replace($this->entity->id(), static::$secondCreatedEntityId, $this->entity->toUrl('canonical')->setAbsolute(TRUE)->toString())], $response->getHeader('Location')); } + 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.'); + return; + } + + $this->initAuthentication(); + $has_canonical_url = $this->entity->hasLinkTemplate('canonical'); + + // Try with all of the following request bodies. + $unparseable_request_body = '!{>}<'; + $parseable_valid_request_body = $this->serializer->encode($this->getNormalizedPatchEntity(), static::$format); + $parseable_valid_request_body_2 = $this->serializer->encode($this->getNormalizedPatchEntity(), static::$format); + $parseable_invalid_request_body = $this->serializer->encode($this->makeNormalizationInvalid($this->getNormalizedPatchEntity()), static::$format); + $parseable_invalid_request_body_2 = $this->serializer->encode($this->getNormalizedPatchEntity() + ['field_rest_test' => [['value' => $this->randomString()]]], static::$format); + + // The URL and Guzzle request options that will be used in this test. The + // request options will be modified/expanded throughout this test: + // - to first test all mistakes a developer might make, and assert that the + // error responses provide a good DX + // - to eventually result in a well-formed request that succeeds. + $url = $this->getUrl(); + $request_options = []; + + + // DX: 405 when resource not provisioned, but HTML if canonical route. + $response = $this->request('PATCH', $url, $request_options); + if ($has_canonical_url) { + $this->assertSame(405, $response->getStatusCode()); + $this->assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type')); + } + else { + $this->assertResourceErrorResponse(404, 'No route found for "PATCH ' . str_replace($this->baseUrl, '', $this->getUrl()->setAbsolute()->toString()) . '"', $response); + } + + + $url->setOption('query', ['_format' => static::$format]); + + + // DX: 405 when resource not provisioned. + $response = $this->request('PATCH', $url, $request_options); + // @todo Open issue to improve this message: it should list '/relative/url?query-string', not just '/relative/url'. + $this->assertResourceErrorResponse(405, 'No route found for "PATCH ' . str_replace($this->baseUrl, '', $this->getUrl()->setAbsolute()->toString()) . '": Method Not Allowed (Allow: GET, POST, HEAD)', $response); + + + $this->provisionEntityResource(); + // Simulate the developer again forgetting the ?_format query string. + $url->setOption('query', []); + + + // DX: 415 when no Content-Type request header, but HTML if canonical route. + $response = $this->request('PATCH', $url, $request_options); + if ($has_canonical_url) { + $this->assertSame(415, $response->getStatusCode()); + $this->assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type')); + $this->assertTrue(FALSE !== strpos($response->getBody()->getContents(), htmlspecialchars('No "Content-Type" request header specified'))); + } + else { + $this->assertResourceErrorResponse(415, 'No "Content-Type" request header specified', $response); + } + + + $url->setOption('query', ['_format' => static::$format]); + + + // DX: 415 when no Content-Type request header. + $response = $this->request('PATCH', $url, $request_options); + $this->assertResourceErrorResponse(415, 'No "Content-Type" request header specified', $response); + + + $request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType; + + + // DX: 400 when no request body. + $response = $this->request('PATCH', $url, $request_options); + $this->assertResourceErrorResponse(400, 'No entity content received.', $response); + + + $request_options[RequestOptions::BODY] = $unparseable_request_body; + + + // DX: 400 when unparseable request body. + $response = $this->request('PATCH', $url, $request_options); + // @todo use this commented line instead of the 3 lines thereafter once https://www.drupal.org/node/2813853 lands. +// $this->assertResourceErrorResponse(400, 'Syntax error', $response); + $this->assertSame(400, $response->getStatusCode()); + $this->assertSame([static::$mimeType], $response->getHeader('Content-Type')); + $this->assertSame($this->serializer->encode(['error' => 'Syntax error'], static::$format), (string) $response->getBody()); + + + + $request_options[RequestOptions::BODY] = $parseable_invalid_request_body; + + + if (static::$auth) { + // DX: forgetting authentication: authentication provider-specific error + // response. + $response = $this->request('PATCH', $url, $request_options); + $this->assertResponseWhenMissingAuthentication($response); + } + + + $request_options = array_merge_recursive($request_options, $this->getAuthenticationRequestOptions('PATCH')); + + + // DX: 403 when unauthorized. + $response = $this->request('PATCH', $url, $request_options); + // @todo Update this to the improved error message when https://www.drupal.org/node/2808233 lands. + $this->assertResourceErrorResponse(403, '', $response); + + + $this->setUpAuthorization('PATCH'); + + + // 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_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); +// $this->assertSame(422, $response->getStatusCode()); +// $this->assertSame([static::$mimeType], $response->getHeader('Content-Type')); + $this->assertSame($this->serializer->encode(['message' => "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n"], static::$format), (string) $response->getBody()); + + + $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); + + + $request_options[RequestOptions::BODY] = $parseable_valid_request_body; + + + // Before sending a well-formed request, allow the authentication provider's + // edge cases to also be tested. + $this->assertAuthenticationEdgeCases('PATCH', $url, $request_options); + + // 200 for well-formed request. + $response = $this->request('PATCH', $url, $request_options); + $this->assertResourceResponse(200, FALSE, $response); + + + $this->config('rest.settings')->set('bc_entity_resource_permissions', TRUE)->save(TRUE); + $request_options[RequestOptions::BODY] = $parseable_valid_request_body_2; + // @todo Remove this in https://www.drupal.org/node/2815845. + drupal_flush_all_caches(); + + + // DX: 403 when unauthorized. + $response = $this->request('PATCH', $url, $request_options); + $this->assertResourceErrorResponse(403, '', $response); + + + $this->grantPermissionsToTestedRole(['restful patch entity:' . static::$entityType]); + + + // 200 for well-formed request. + $response = $this->request('PATCH', $url, $request_options); + $this->assertResourceResponse(200, FALSE, $response); + } + /** * Gets an entity resource's POST URL. * @@ -523,11 +703,15 @@ protected function getPostUrl() { } /** - * Decorates ::getNormalizedEntityToCreate(). + * Makes the given entity normalization invalid. + * + * @param array $normalization + * An entity normalization. + * + * @return array + * The updated entity normalization, now invalid. */ - protected function getInvalidNormalizedEntityToCreate() { - $normalization = $this->getNormalizedPostEntity(); - + 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; $normalization[$label_field][1]['value'] = 'Second Title'; 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 abb06d9..418f6ee 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php @@ -29,6 +29,9 @@ protected function setUpAuthorization($method) { case 'POST': $this->grantPermissionsToTestedRole(['create entity_test entity_test_with_bundle entities']); break; + case 'PATCH': + $this->grantPermissionsToTestedRole(['administer entity_test content']); + break; } } 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 d1d9ae7..28d2807 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php @@ -30,6 +30,9 @@ protected function setUpAuthorization($method) { case 'POST': $this->grantPermissionsToTestedRole(['access content', 'create camelids content']); break; + case 'PATCH': + $this->grantPermissionsToTestedRole(['access content', 'edit any camelids content']); + break; } } 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 01786e5..81b4af1 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php @@ -30,6 +30,10 @@ protected function setUpAuthorization($method) { // @todo Create issue similar to https://www.drupal.org/node/2808217. $this->grantPermissionsToTestedRole(['administer taxonomy']); break; + case 'PATCH': + // @todo Create issue similar to https://www.drupal.org/node/2808217. + $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 ec6e9d6..264de57 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php @@ -43,6 +43,9 @@ protected function setUpAuthorization($method) { case 'POST': $this->grantPermissionsToTestedRole(['administer users']); break; + case 'PATCH': + $this->grantPermissionsToTestedRole(['administer users']); + break; } }