.../EntityTest/EntityTestHalJsonAnonTest.php | 13 ++
.../EntityResource/Node/NodeHalJsonAnonTest.php | 13 ++
.../EntityResource/Term/TermHalJsonAnonTest.php | 13 ++
.../EntityResource/Block/BlockResourceTestBase.php | 2 +-
.../ConfigTest/ConfigTestResourceTestBase.php | 2 +-
.../EntityResource/EntityResourceTestBase.php | 143 ++++++++++++++++++---
.../EntityTest/EntityTestResourceTestBase.php | 25 +++-
.../EntityResource/Node/NodeResourceTestBase.php | 30 ++++-
.../EntityResource/Role/RoleResourceTestBase.php | 2 +-
.../EntityResource/Term/TermResourceTestBase.php | 30 ++++-
.../Vocabulary/VocabularyResourceTestBase.php | 2 +-
.../rest/tests/src/Functional/ResourceTestBase.php | 42 +++++-
12 files changed, 283 insertions(+), 34 deletions(-)
diff --git a/core/modules/hal/tests/src/Functional/EntityResource/EntityTest/EntityTestHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/EntityResource/EntityTest/EntityTestHalJsonAnonTest.php
index 935a899..cb0d544 100644
--- a/core/modules/hal/tests/src/Functional/EntityResource/EntityTest/EntityTestHalJsonAnonTest.php
+++ b/core/modules/hal/tests/src/Functional/EntityResource/EntityTest/EntityTestHalJsonAnonTest.php
@@ -80,4 +80,17 @@ protected function getExpectedNormalizedEntity() {
];
}
+ /**
+ * {@inheritdoc}
+ */
+ protected function getNormalizedEntityToCreate() {
+ return parent::getNormalizedEntityToCreate() + [
+ '_links' => [
+ 'type' => [
+ 'href' => $this->baseUrl . '/rest/type/entity_test/entity_test',
+ ],
+ ],
+ ];
+ }
+
}
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 c926da7..cb22d58 100644
--- a/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php
+++ b/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php
@@ -101,4 +101,17 @@ protected function getExpectedNormalizedEntity() {
];
}
+ /**
+ * {@inheritdoc}
+ */
+ protected function getNormalizedEntityToCreate() {
+ return parent::getNormalizedEntityToCreate() + [
+ '_links' => [
+ 'type' => [
+ 'href' => $this->baseUrl . '/rest/type/node/camelids',
+ ],
+ ],
+ ];
+ }
+
}
diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Term/TermHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/EntityResource/Term/TermHalJsonAnonTest.php
index b2b2bee..6fd2279 100644
--- a/core/modules/hal/tests/src/Functional/EntityResource/Term/TermHalJsonAnonTest.php
+++ b/core/modules/hal/tests/src/Functional/EntityResource/Term/TermHalJsonAnonTest.php
@@ -54,4 +54,17 @@ protected function getExpectedNormalizedEntity() {
];
}
+ /**
+ * {@inheritdoc}
+ */
+ protected function getNormalizedEntityToCreate() {
+ return parent::getNormalizedEntityToCreate() + [
+ '_links' => [
+ 'type' => [
+ 'href' => $this->baseUrl . '/rest/type/taxonomy_term/camelids',
+ ],
+ ],
+ ];
+ }
+
}
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 f77a522..8c692b6 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockResourceTestBase.php
@@ -22,7 +22,7 @@
/**
* {@inheritdoc}
*/
- protected function setUpAuthorization() {
+ protected function setUpAuthorization($method) {
$this->grantPermissionsToTestedRole(['administer blocks']);
}
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 36315f3..be91643 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestResourceTestBase.php
@@ -19,7 +19,7 @@
/**
* {@inheritdoc}
*/
- protected function setUpAuthorization() {
+ protected function setUpAuthorization($method) {
$this->grantPermissionsToTestedRole(['view config_test']);
}
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
index 80a21d2..ded166b 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -3,10 +3,12 @@
namespace Drupal\Tests\rest\Functional\EntityResource;
use Drupal\Component\Serialization\Json;
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\EntityType;
use Drupal\Core\Url;
use Drupal\Tests\rest\Functional\ResourceTestBase;
use GuzzleHttp\Exception\ClientException;
+use GuzzleHttp\RequestOptions;
use Psr\Http\Message\ResponseInterface;
/**
@@ -64,6 +66,9 @@ public function setUp() {
// Create an entity.
$this->entity = $this->createEntity();
+
+ // @todo Remove this in https://www.drupal.org/node/2815845.
+ drupal_flush_all_caches();
}
/**
@@ -86,7 +91,7 @@ public function setUp() {
public function testGet() {
// @todo test what happens before provisioning. Edge case: node.
- $this->setUpAuthorization();
+ $this->setUpAuthorization('GET');
// Provision REST resource.
$this->provisionEntityResource();
@@ -210,29 +215,127 @@ public function testGet() {
}
}
- // @todo try the following request bodies:
- // 1. empty (only makes sense for DELETE and GET), assert 400
- // 2. not parseable, assert 400
- // 3. parseable but invalid (with various possible errors, possibly entity type specific ones?), assert 422
- // 4. parseable and valid, assert 201
- public function atestPost() {
- // Attempt to use REST resource while not provisioned.
- $this->performUnsafeOperation('POST');
- $this->assertSame(404, $this->getSession()->getStatusCode());
- $this->assertFalse(EntityType::loadMultiple(), 'No entity has been created in the database.');
+ 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.');
+ return;
+ }
+
+ // Try with all of the following request bodies.
+ $unparseable_request_body = '!{>}<';
+ $parseable_valid_request_body = $this->serializer->encode($this->getNormalizedEntityToCreate(), static::$format);
+ $parseable_invalid_request_body = $this->serializer->encode($this->getInvalidNormalizedEntityToCreate(), 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->getPostUrl()->setOption('query', ['_format' => static::$format]);
+ $request_options = [];
+
+
+ // DX: 404 when resource not provisioned.
+ $response = $this->request('POST', $url, $request_options);
+ // @todo Open issue to improve this message: it should list '/relative/url?query-string', not just '/relative/url'.
+ $this->assertResourceErrorResponse(404, 'No route found for "POST ' . $this->getPostUrl()->toString() . '"', $response);
- // Provision REST resource.
$this->provisionEntityResource();
- // Try as user without 'create' access for this entity type.
- $this->performUnsafeOperation('POST');
- $this->assertSame(403, $this->getSession()->getStatusCode());
- $this->assertSession()->responseContains('not allowed …');
- // Try as user with sufficient permissions.
- $this->performUnsafeOperation('POST');
- $this->assertSame(201, $this->getSession()->getStatusCode());
- $this->assertSession()->responseContains('…');
+ // DX: 415 when no Content-Type request header.
+ $response = $this->request('POST', $url, $request_options);
+ // @todo update once https://www.drupal.org/node/2811133 lands.
+ $this->assertResourceErrorResponse(415, 'No route found that matches "Content-Type: "', $response);
+
+
+ $request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType;
+
+
+ // DX: 400 when no request body.
+ $response = $this->request('POST', $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('POST', $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('POST', $url, $request_options);
+ $this->verifyResponseWhenMissingAuthentication($response);
+ }
+
+
+ $request_options = array_merge_recursive($request_options, $this->getAuthenticationRequestOptions());
+
+
+ // DX: 403 when unauthorized.
+ $response = $this->request('POST', $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('POST');
+
+
+ // DX: 422 when invalid entity.
+ $response = $this->request('POST', $url, $request_options);
+ $label_field = $this->entity->getEntityType()->getKey('label');
+ $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_valid_request_body;
+
+
+ // 201 for well-formed request.
+ $response = $this->request('POST', $url, $request_options);
+ $this->assertResourceResponse(201, FALSE, $response);
+ $this->assertSame([str_replace($this->entity->id(), 2, $this->entity->toUrl('canonical')->setAbsolute(TRUE)->toString())], $response->getHeader('Location'));
+ }
+
+ /**
+ * Gets an entity resource's POST URL.
+ *
+ * @return \Drupal\Core\Url
+ * The URL to POST to.
+ */
+ 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);
+ }
+
+ /**
+ * Decorates ::getNormalizedEntityToCreate().
+ */
+ protected function getInvalidNormalizedEntityToCreate() {
+ $normalization = $this->getNormalizedEntityToCreate();
+
+ // Add a second label to this entity to make it invalid.
+ $label_field = $this->entity->getEntityType()->getKey('label');
+ $normalization[$label_field][1]['value'] = 'Second Title';
+
+ return $normalization;
}
/**
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 f4fc77f..deaf1e8 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php
@@ -21,8 +21,15 @@
/**
* {@inheritdoc}
*/
- protected function setUpAuthorization() {
- $this->grantPermissionsToTestedRole(['view test entity']);
+ protected function setUpAuthorization($method) {
+ switch ($method) {
+ case 'GET':
+ $this->grantPermissionsToTestedRole(['view test entity']);
+ break;
+ case 'POST':
+ $this->grantPermissionsToTestedRole(['create entity_test entity_test_with_bundle entities']);
+ break;
+ }
}
/**
@@ -87,4 +94,18 @@ protected function getExpectedNormalizedEntity() {
return $normalization;
}
+ /**
+ * {@inheritdoc}
+ */
+ protected function getNormalizedEntityToCreate() {
+ return [
+ 'type' => 'entity_test',
+ 'name' => [
+ [
+ 'value' => 'Dramallama',
+ ],
+ ],
+ ];
+ }
+
}
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 e77d1ff..f32105e 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php
@@ -22,8 +22,15 @@
/**
* {@inheritdoc}
*/
- protected function setUpAuthorization() {
- $this->grantPermissionsToTestedRole(['access content']);
+ protected function setUpAuthorization($method) {
+ switch ($method) {
+ case 'GET':
+ $this->grantPermissionsToTestedRole(['access content']);
+ break;
+ case 'POST':
+ $this->grantPermissionsToTestedRole(['access content', 'create camelids content']);
+ break;
+ }
}
/**
@@ -139,5 +146,22 @@ protected function getExpectedNormalizedEntity() {
];
}
- // methods to provide one-off expectations/data/…
+ /**
+ * {@inheritdoc}
+ */
+ protected function getNormalizedEntityToCreate() {
+ return [
+ 'type' => [
+ [
+ 'target_id' => 'camelids',
+ ],
+ ],
+ 'title' => [
+ [
+ 'value' => 'Dramallama',
+ ],
+ ],
+ ];
+ }
+
}
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 091a84a..f5406d4 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Role/RoleResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Role/RoleResourceTestBase.php
@@ -21,7 +21,7 @@
/**
* {@inheritdoc}
*/
- protected function setUpAuthorization() {
+ protected function setUpAuthorization($method) {
$this->grantPermissionsToTestedRole(['administer permissions']);
}
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 cddb4c8..77372df 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php
@@ -21,8 +21,16 @@
/**
* {@inheritdoc}
*/
- protected function setUpAuthorization() {
- $this->grantPermissionsToTestedRole(['access content']);
+ protected function setUpAuthorization($method) {
+ switch ($method) {
+ case 'GET':
+ $this->grantPermissionsToTestedRole(['access content']);
+ break;
+ case 'POST':
+ // @todo Create issue similar to https://www.drupal.org/node/2808217.
+ $this->grantPermissionsToTestedRole(['administer taxonomy']);
+ break;
+ }
}
/**
@@ -92,4 +100,22 @@ protected function getExpectedNormalizedEntity() {
];
}
+ /**
+ * {@inheritdoc}
+ */
+ protected function getNormalizedEntityToCreate() {
+ return [
+ 'vid' => [
+ [
+ 'target_id' => 'camelids',
+ ],
+ ],
+ 'name' => [
+ [
+ 'value' => 'Dramallama',
+ ],
+ ],
+ ];
+ }
+
}
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 d2522ba..235dc7d 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Vocabulary/VocabularyResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Vocabulary/VocabularyResourceTestBase.php
@@ -23,7 +23,7 @@
/**
* {@inheritdoc}
*/
- protected function setUpAuthorization() {
+ protected function setUpAuthorization($method) {
$this->grantPermissionsToTestedRole(['administer taxonomy']);
}
diff --git a/core/modules/rest/tests/src/Functional/ResourceTestBase.php b/core/modules/rest/tests/src/Functional/ResourceTestBase.php
index 39f7da3..a4d133b 100644
--- a/core/modules/rest/tests/src/Functional/ResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/ResourceTestBase.php
@@ -2,10 +2,12 @@
namespace Drupal\Tests\rest\Functional;
+use Drupal\Core\Url;
use Drupal\rest\RestResourceConfigInterface;
use Drupal\Tests\BrowserTestBase;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
+use GuzzleHttp\Exception\ClientException;
use Psr\Http\Message\ResponseInterface;
/**
@@ -140,8 +142,8 @@ protected function provisionResource($resource_type, $formats = [], $authenticat
'authentication' => $authentication,
]
])->save();
-
- $this->container->get('router.builder')->rebuild();
+ // @todo Remove this in https://www.drupal.org/node/2815845.
+ drupal_flush_all_caches();
}
/**
@@ -153,12 +155,19 @@ protected function provisionResource($resource_type, $formats = [], $authenticat
* In case of a test verifying behavior when using a particular authentication
* provider: create a user with a particular set of permissions.
*
+ * Because of the $method parameter, it's possible to first set up
+ * authentication for only GET, then add POST, et cetera. This then also
+ * allows for verifying a 403 in case of missing authorization.
+ *
+ * @param string $method
+ * The HTTP method for which to set up authentication.
+ *
* @return void
*
* @see ::grantPermissionsToAnonymousRole()
* @see ::grantPermissionsToAuthenticatedRole()
*/
- abstract protected function setUpAuthorization();
+ abstract protected function setUpAuthorization($method);
/**
* Verifies the error response in case of missing authentication.
@@ -217,6 +226,33 @@ protected function grantPermissionsToTestedRole(array $permissions) {
}
}
+ /**
+ * Performs a HTTP request. Wraps the Guzzle HTTP client.
+ *
+ * Why wrap the Guzzle HTTP client? Because any error response is returned via
+ * an exception, which would make the tests unnecessarily complex to read.
+ *
+ * @see \GuzzleHttp\ClientInterface::request()
+ *
+ * @param string $method
+ * HTTP method.
+ * @param \Drupal\Core\Url $url
+ * URL to request.
+ * @param array $request_options
+ * Request options to apply.
+ *
+ * @return \Psr\Http\Message\ResponseInterface
+ */
+ protected function request($method, Url $url, array $request_options) {
+ try {
+ $response = $this->httpClient->request($method, $url->toString(), $request_options);
+ }
+ catch (ClientException $e) {
+ $response = $e->getResponse();
+ }
+ return $response;
+ }
+
/**
* Asserts that a resource response has the given status code and body.
*