.../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;
}
}