From 1129eec3b70ec9ff8b412746cf295f07b548f4fb Mon Sep 17 00:00:00 2001 From: Wim Leers Date: Wed, 14 Sep 2016 18:39:04 +0200 Subject: [PATCH 1/6] rest tests --- .../rest/src/Tests/EntityResourceTestBase.php | 164 +++++++++++++++++++++ .../rest/src/Tests/NodeHalJsonBasicAuthTest.php | 43 ++++++ core/modules/rest/src/Tests/ResourceTestBase.php | 85 +++++++++++ 3 files changed, 292 insertions(+) create mode 100644 core/modules/rest/src/Tests/EntityResourceTestBase.php create mode 100644 core/modules/rest/src/Tests/NodeHalJsonBasicAuthTest.php create mode 100644 core/modules/rest/src/Tests/ResourceTestBase.php diff --git a/core/modules/rest/src/Tests/EntityResourceTestBase.php b/core/modules/rest/src/Tests/EntityResourceTestBase.php new file mode 100644 index 0000000..41a10e5 --- /dev/null +++ b/core/modules/rest/src/Tests/EntityResourceTestBase.php @@ -0,0 +1,164 @@ +provisionResource('entity.' . static::$entityType, static::$format, static::$auth); + } + + public function setUp() { + parent::setUp(); + + // Set up a HTTP client that accepts relative URLs. + $this->httpClient = $this->container->get('http_client_factory') + ->fromOptions(['base_uri' => $this->baseUrl]); + + // Add field with specific allowed value. + // (allows testing invalid vs valid field value) + + // Add access-protected field to entity type. + // (allows testing with field that cannot be modified) + + // Create an entity. + $this->entity = $this->createEntity(); + } + + /** + * Creates the entity to be tested. + * + * @return \Drupal\Core\Entity\EntityInterface + * The entity to be tested. + */ + abstract protected function createEntity(); + + public function testGet() { + /** @var \Psr\Http\Message\ResponseInterface $response */ + $url = $this->entity->toUrl(); + + // Requesting without the appropriate ?_format query argument. + $response = $this->httpClient->get($url->toString()); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type')); + + $url->setOption('query', ['_format' => static::$format]); + + // Verify that the entity exists: use a HEAD request. + $response = $this->httpClient->head($url->toString()); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('', (string)$response->getBody()); + $this->assertSame(['MISS'], $response->getHeader('X-Drupal-Cache')); + $head_headers = $response->getHeaders(); + + // Now do a GET request. And because of the preceding HEAD request, this + // should be a Page Cache hit. + $response = $this->httpClient->get($url->toString()); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame(['application/hal+json'], $response->getHeader('Content-Type')); + $this->assertSame(['HIT'], $response->getHeader('X-Drupal-Cache')); + $get_headers = $response->getHeaders(); + + // Verify that the GET and HEAD responses are the same, that the only + // difference is that there's no body. + $ignored_headers = ['Date', 'Content-Length', 'X-Drupal-Cache', 'X-Drupal-Dynamic-Page-Cache']; + foreach ($ignored_headers as $ignored_header) { + unset($head_headers[$ignored_header]); + unset($get_headers[$ignored_header]); + } + $this->assertSame($get_headers, $head_headers); + + // Try to read the entity with an unsupported mime format. + try { + $url->setOption('query', ['_format' => 'non_existing_format']); + $this->httpClient->get($url->toString()); + $this->fail('No 406 response received.'); + } + catch (ClientException $e) { + $response = $e->getResponse(); + $this->assertSame(406, $response->getStatusCode()); + $this->assertSame(['application/json'], $response->getHeader('Content-Type')); + } + + + + } + + // @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.'); + + // 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('…'); + } + + /** + * Simulate common developer mistake when performing an unsafe operation: + * - forget to specify the X-CSRF-Token request header + * - specify in invalid X-CSRF-Token request header value + * + * In either case, the REST module must provide meaningful feedback for DX. + */ + protected function performUnsafeOperation($method) { + // Try without CSRF token + // …request + $this->assertSame(403, $this->getSession()->getStatusCode()); + $this->assertSession()->responseContains('X-CSRF-Token request header is missing'); + // Try with invalid CSRF token + // …request + $this->assertSame(403, $this->getSession()->getStatusCode()); + $this->assertSession()->responseContains('X-CSRF-Token request header is invalid'); + // Try with valid CSRF token + // …request + } + +} diff --git a/core/modules/rest/src/Tests/NodeHalJsonBasicAuthTest.php b/core/modules/rest/src/Tests/NodeHalJsonBasicAuthTest.php new file mode 100644 index 0000000..f2d6029 --- /dev/null +++ b/core/modules/rest/src/Tests/NodeHalJsonBasicAuthTest.php @@ -0,0 +1,43 @@ + 'Camelids', + 'type' => 'camelids', + ])->save(); + + // Create a "Llama" node. + $node = Node::create(['type' => 'camelids']); + $node->setTitle('Llama') + ->setPublished(TRUE) + ->save(); + + return $node; + } + + + // methods to provide one-off expectations/data/… +} diff --git a/core/modules/rest/src/Tests/ResourceTestBase.php b/core/modules/rest/src/Tests/ResourceTestBase.php new file mode 100644 index 0000000..64527ea --- /dev/null +++ b/core/modules/rest/src/Tests/ResourceTestBase.php @@ -0,0 +1,85 @@ +resourceConfigStorage = $this->container->get('entity_type.manager')->getStorage('rest_resource_config'); + } + + /** + * Provisions a REST resource. + * + * @param string $resource_type + * The resource type (REST resource plugin ID). + * @param string[] $formats + * The allowed formats for this resource. + * @param string[] $authentication + * The allowed authentication providers for this resource. + */ + protected function provisionResource($resource_type, $formats = [], $authentication = []) { + $this->resourceConfigStorage->create([ + 'id' => $resource_type, + 'granularity' => RestResourceConfigInterface::RESOURCE_GRANULARITY, + 'configuration' => [ + 'methods' => ['GET', 'POST', 'PATCH', 'DELETE'], + 'formats' => $formats, + 'authentication' => $authentication, + ] + ])->save(); + //$this->rebuildCache(); + } + +} -- 2.9.0 From 538a3726386b1f20c6d0147acaf605f0eb266c06 Mon Sep 17 00:00:00 2001 From: Wim Leers Date: Wed, 14 Sep 2016 20:10:15 +0200 Subject: [PATCH 2/6] basics working --- .../rest/src/Tests/EntityResourceTestBase.php | 39 +++++++++++++++++++--- .../rest/src/Tests/NodeHalJsonBasicAuthTest.php | 23 ++++++++++++- .../rest/src/Tests/NodeJsonBasicAuthTest.php | 0 .../rest/src/Tests/NodeResourceTestBase.php | 0 core/modules/rest/src/Tests/ResourceTestBase.php | 20 +++++++++++ 5 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 core/modules/rest/src/Tests/NodeJsonBasicAuthTest.php create mode 100644 core/modules/rest/src/Tests/NodeResourceTestBase.php diff --git a/core/modules/rest/src/Tests/EntityResourceTestBase.php b/core/modules/rest/src/Tests/EntityResourceTestBase.php index 41a10e5..3b14c0c 100644 --- a/core/modules/rest/src/Tests/EntityResourceTestBase.php +++ b/core/modules/rest/src/Tests/EntityResourceTestBase.php @@ -2,7 +2,9 @@ namespace Drupal\rest\Tests; +use Drupal\Component\Serialization\Json; use Drupal\Core\Entity\EntityType; +use Drupal\Core\Url; use GuzzleHttp\Exception\ClientException; /** @@ -79,8 +81,8 @@ public function testGet() { // Verify that the entity exists: use a HEAD request. $response = $this->httpClient->head($url->toString()); $this->assertSame(200, $response->getStatusCode()); - $this->assertSame('', (string)$response->getBody()); $this->assertSame(['MISS'], $response->getHeader('X-Drupal-Cache')); + $this->assertSame('', (string)$response->getBody()); $head_headers = $response->getHeaders(); // Now do a GET request. And because of the preceding HEAD request, this @@ -89,6 +91,7 @@ public function testGet() { $this->assertSame(200, $response->getStatusCode()); $this->assertSame(['application/hal+json'], $response->getHeader('Content-Type')); $this->assertSame(['HIT'], $response->getHeader('X-Drupal-Cache')); + $this->assertSame('……………………………', (string)$response->getBody()); $get_headers = $response->getHeaders(); // Verify that the GET and HEAD responses are the same, that the only @@ -100,7 +103,7 @@ public function testGet() { } $this->assertSame($get_headers, $head_headers); - // Try to read the entity with an unsupported mime format. + // Try to read the entity in an unsupported format. try { $url->setOption('query', ['_format' => 'non_existing_format']); $this->httpClient->get($url->toString()); @@ -109,11 +112,39 @@ public function testGet() { catch (ClientException $e) { $response = $e->getResponse(); $this->assertSame(406, $response->getStatusCode()); - $this->assertSame(['application/json'], $response->getHeader('Content-Type')); + $this->assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type')); } + // Repeat the same request, but this time with the format's MIME type in the + // Accept header. + try { + $headers = ['Accept' => static::$mimeType]; + $url->setOption('query', ['_format' => 'non_existing_format']); + $this->httpClient->get($url->toString(), ['headers' => $headers]); + $this->fail('No 406 response received.'); + } + catch (ClientException $e) { + $response = $e->getResponse(); + $this->assertSame(406, $response->getStatusCode()); + $this->assertSame([static::$expectedErrorMimeType], $response->getHeader('Content-Type')); + } - + // Try to read an entity that does not exist. + $non_existing_entity_url = Url::fromRoute('rest.entity.' . static::$entityType . '.GET.' . static::$format); + $non_existing_entity_url->setRouteParameter(static::$entityType, 987654321); + $non_existing_entity_url->setOption('query', ['_format' => static::$format]); + try { + $this->httpClient->get($non_existing_entity_url->toString()); + $this->fail('No 404 response received.'); + } + catch (ClientException $e) { + $response = $e->getResponse(); + $this->assertSame(404, $response->getStatusCode()); + $this->assertSame([static::$expectedErrorMimeType], $response->getHeader('Content-Type')); + $path = str_replace('987654321', '{' . static::$entityType . '}', $non_existing_entity_url->setOption('query', [])->toString()); + $expected_message = Json::encode(['message' => 'The "' . static::$entityType . '" parameter was not converted for the path "' . $path . '" (route name: "rest.entity.' . static::$entityType . '.GET.' . static::$format . '")']); + $this->assertSame($expected_message, (string)$response->getBody()); + } } // @todo try the following request bodies: diff --git a/core/modules/rest/src/Tests/NodeHalJsonBasicAuthTest.php b/core/modules/rest/src/Tests/NodeHalJsonBasicAuthTest.php index f2d6029..af86d16 100644 --- a/core/modules/rest/src/Tests/NodeHalJsonBasicAuthTest.php +++ b/core/modules/rest/src/Tests/NodeHalJsonBasicAuthTest.php @@ -6,7 +6,7 @@ use Drupal\node\Entity\NodeType; /** - * @group restbetter + * @group rest */ class NodeHalJsonBasicAuthTest extends EntityResourceTestBase { @@ -15,8 +15,29 @@ class NodeHalJsonBasicAuthTest extends EntityResourceTestBase { */ public static $modules = ['node', 'hal', 'basic_auth']; + /** + * {@inheritdoc} + */ protected static $entityType = 'node'; + + /** + * {@inheritdoc} + */ protected static $format = 'hal_json'; + + /** + * {@inheritdoc} + */ + protected static $mimeType = 'application/hal+json'; + + /** + * {@inheritdoc} + */ + protected static $expectedErrorMimeType = 'application/json'; + + /** + * {@inheritdoc} + */ protected static $auth = 'basic_auth'; /** diff --git a/core/modules/rest/src/Tests/NodeJsonBasicAuthTest.php b/core/modules/rest/src/Tests/NodeJsonBasicAuthTest.php new file mode 100644 index 0000000..e69de29 diff --git a/core/modules/rest/src/Tests/NodeResourceTestBase.php b/core/modules/rest/src/Tests/NodeResourceTestBase.php new file mode 100644 index 0000000..e69de29 diff --git a/core/modules/rest/src/Tests/ResourceTestBase.php b/core/modules/rest/src/Tests/ResourceTestBase.php index 64527ea..0b298f5 100644 --- a/core/modules/rest/src/Tests/ResourceTestBase.php +++ b/core/modules/rest/src/Tests/ResourceTestBase.php @@ -25,6 +25,26 @@ class ResourceTestBase extends BrowserTestBase { */ protected static $format = 'json'; + /** + * The MIME type that corresponds to $format. + * + * (Sadly this cannot be computed automatically yet.) + * + * @var string + */ + protected static $mimeType = 'application/json'; + + /** + * The expected MIME type in case of 4xx error responses. + * + * (Can be different, when $mimeType for example encodes a particular + * normalization, such as 'application/hal+json': its error response MIME + * type is 'application/json'.) + * + * @var string + */ + protected static $expectedErrorMimeType = 'application/json'; + /** * The authentication mechanism to use in this test. * -- 2.9.0 From 17d1c3f13654fc2f236a8171ec211a334e1b2568 Mon Sep 17 00:00:00 2001 From: Wim Leers Date: Thu, 15 Sep 2016 16:33:06 +0200 Subject: [PATCH 3/6] now this is actually getting a decent test --- .../rest/src/Tests/EntityResourceTestBase.php | 20 +++- .../rest/src/Tests/NodeHalJsonBasicAuthTest.php | 84 +++++++++----- .../rest/src/Tests/NodeJsonBasicAuthTest.php | 37 +++++++ .../rest/src/Tests/NodeResourceTestBase.php | 123 +++++++++++++++++++++ core/modules/rest/src/Tests/ResourceTestBase.php | 6 +- 5 files changed, 241 insertions(+), 29 deletions(-) diff --git a/core/modules/rest/src/Tests/EntityResourceTestBase.php b/core/modules/rest/src/Tests/EntityResourceTestBase.php index 3b14c0c..9be05ed 100644 --- a/core/modules/rest/src/Tests/EntityResourceTestBase.php +++ b/core/modules/rest/src/Tests/EntityResourceTestBase.php @@ -39,7 +39,7 @@ protected function provisionEntityResource() { - $this->provisionResource('entity.' . static::$entityType, static::$format, static::$auth); + $this->provisionResource('entity.' . static::$entityType, [static::$format], [static::$auth]); } public function setUp() { @@ -67,7 +67,21 @@ public function setUp() { */ abstract protected function createEntity(); + /** + * Returns the expected normalization of the entity. + * + * @see ::createEntity() + * + * @return array + */ + abstract protected function getExpectedNormalizedEntity(); + public function testGet() { + // @todo test what happens before provisioning. Edge case: node. + + // Provision REST resource. + $this->provisionEntityResource(); + /** @var \Psr\Http\Message\ResponseInterface $response */ $url = $this->entity->toUrl(); @@ -89,9 +103,9 @@ public function testGet() { // should be a Page Cache hit. $response = $this->httpClient->get($url->toString()); $this->assertSame(200, $response->getStatusCode()); - $this->assertSame(['application/hal+json'], $response->getHeader('Content-Type')); + $this->assertSame([static::$mimeType], $response->getHeader('Content-Type')); $this->assertSame(['HIT'], $response->getHeader('X-Drupal-Cache')); - $this->assertSame('……………………………', (string)$response->getBody()); + $this->assertEquals($this->getExpectedNormalizedEntity(), Json::decode((string)$response->getBody())); $get_headers = $response->getHeaders(); // Verify that the GET and HEAD responses are the same, that the only diff --git a/core/modules/rest/src/Tests/NodeHalJsonBasicAuthTest.php b/core/modules/rest/src/Tests/NodeHalJsonBasicAuthTest.php index af86d16..2679ad1 100644 --- a/core/modules/rest/src/Tests/NodeHalJsonBasicAuthTest.php +++ b/core/modules/rest/src/Tests/NodeHalJsonBasicAuthTest.php @@ -2,23 +2,17 @@ namespace Drupal\rest\Tests; -use Drupal\node\Entity\Node; -use Drupal\node\Entity\NodeType; +use Drupal\user\Entity\User; /** * @group rest */ -class NodeHalJsonBasicAuthTest extends EntityResourceTestBase { +class NodeHalJsonBasicAuthTest extends NodeResourceTestBase { /** * {@inheritdoc} */ - public static $modules = ['node', 'hal', 'basic_auth']; - - /** - * {@inheritdoc} - */ - protected static $entityType = 'node'; + public static $modules = ['hal', 'basic_auth']; /** * {@inheritdoc} @@ -43,22 +37,62 @@ class NodeHalJsonBasicAuthTest extends EntityResourceTestBase { /** * {@inheritdoc} */ - protected function createEntity() { - // Create a "Camelids" node type. - NodeType::create([ - 'name' => 'Camelids', - 'type' => 'camelids', - ])->save(); - - // Create a "Llama" node. - $node = Node::create(['type' => 'camelids']); - $node->setTitle('Llama') - ->setPublished(TRUE) - ->save(); - - return $node; + protected function getExpectedNormalizedEntity() { + $author = User::load(0); + return parent::getExpectedNormalizedEntity() + [ + '_links' => [ + 'self' => [ + 'href' => $this->baseUrl . 'node/1?_format=hal_json', + ], + 'type' => [ + 'href' => $this->baseUrl . 'rest/type/node/camelids', + ], + $this->baseUrl . 'rest/relation/node/camelids/uid' => [ + [ + 'href' => $this->baseUrl . 'user/0?_format=hal_json', + 'lang' => 'en', + ], + ], + $this->baseUrl . 'rest/relation/node/camelids/revision_uid' => [ + [ + 'href' => $this->baseUrl . 'user/0?_format=hal_json', + ], + ], + ], + '_embedded' => [ + $this->baseUrl . 'rest/relation/node/camelids/uid' => [ + [ + '_links' => [ + 'self' => [ + 'href' => $this->baseUrl . 'user/0?_format=hal_json', + ], + 'type' => [ + 'href' => $this->baseUrl . 'rest/type/user/user', + ], + ], + 'uuid' => [ + ['value' => $author->uuid()] + ], + 'lang' => 'en', + ], + ], + $this->baseUrl . 'rest/relation/node/camelids/revision_uid' => [ + [ + '_links' => [ + 'self' => [ + 'href' => $this->baseUrl . 'user/0?_format=hal_json', + ], + 'type' => [ + 'href' => $this->baseUrl . 'rest/type/user/user', + ], + ], + 'uuid' => [ + ['value' => $author->uuid()] + ], + ], + ], + ], + ]; } - - // methods to provide one-off expectations/data/… } diff --git a/core/modules/rest/src/Tests/NodeJsonBasicAuthTest.php b/core/modules/rest/src/Tests/NodeJsonBasicAuthTest.php index e69de29..c5607ee 100644 --- a/core/modules/rest/src/Tests/NodeJsonBasicAuthTest.php +++ b/core/modules/rest/src/Tests/NodeJsonBasicAuthTest.php @@ -0,0 +1,37 @@ + 'Camelids', + 'type' => 'camelids', + ])->save(); + + // Create a "Llama" node. + $node = Node::create(['type' => 'camelids']); + $node->setTitle('Llama') + ->setPublished(TRUE) + ->setCreatedTime(123456789) + ->setChangedTime(123456789) + ->setRevisionCreationTime(123456789) + ->save(); + + return $node; + } + + protected function getExpectedNormalizedEntity() { + assert('$this->entity instanceof \Drupal\Core\Entity\EntityInterface', 'Entity must already have been created.'); + return [ + 'nid' => [ + ['value' => 1], + ], + 'uuid' => [ + ['value' => $this->entity->uuid()], + ], + 'vid' => [ + ['value' => 1], + ], + 'langcode' => [ + [ + 'value' => 'en', + 'lang' => 'en', + ], + ], + 'type' => [ + ['target_id' => 'camelids'], + ], + 'title' => [ + [ + 'value' => 'Llama', + 'lang' => 'en', + ], + ], + 'status' => [ + [ + 'value' => 1, + 'lang' => 'en', + ], + ], + 'created' => [ + [ + 'value' => 123456789, + 'lang' => 'en', + ], + ], + 'changed' => [ + [ + 'value' => 123456789, + 'lang' => 'en', + ], + ], + 'promote' => [ + [ + 'value' => 1, + 'lang' => 'en', + ], + ], + 'sticky' => [ + [ + 'value' => '0', + 'lang' => 'en', + ], + ], + 'revision_timestamp' => [ + [ + 'value' => 123456789, + ], + ], + 'revision_translation_affected' => [ + [ + 'value' => TRUE, + 'lang' => 'en', + ], + ], + 'default_langcode' => [ + [ + 'value' => TRUE, + 'lang' => 'en', + ], + ], + ]; + } + + // methods to provide one-off expectations/data/… +} diff --git a/core/modules/rest/src/Tests/ResourceTestBase.php b/core/modules/rest/src/Tests/ResourceTestBase.php index 0b298f5..19b1c81 100644 --- a/core/modules/rest/src/Tests/ResourceTestBase.php +++ b/core/modules/rest/src/Tests/ResourceTestBase.php @@ -77,6 +77,9 @@ public function setUp() { parent::setUp(); $this->resourceConfigStorage = $this->container->get('entity_type.manager')->getStorage('rest_resource_config'); + + // Ensure there's a clean slate: delete all REST resource config entities. + $this->resourceConfigStorage->delete($this->resourceConfigStorage->loadMultiple()); } /** @@ -99,7 +102,8 @@ protected function provisionResource($resource_type, $formats = [], $authenticat 'authentication' => $authentication, ] ])->save(); - //$this->rebuildCache(); + + $this->container->get('router.builder')->rebuild(); } } -- 2.9.0 From bbe1f79b5c25e247e5d3201eaa26be63e1b443d5 Mon Sep 17 00:00:00 2001 From: Wim Leers Date: Thu, 15 Sep 2016 18:02:53 +0200 Subject: [PATCH 4/6] make it work for both json and hal_json --- .../rest/src/Tests/EntityResourceTestBase.php | 12 ++++++- .../rest/src/Tests/NodeHalJsonBasicAuthTest.php | 22 +++++++++++- .../rest/src/Tests/NodeResourceTestBase.php | 41 +++++++++++++++------- core/modules/rest/src/Tests/ResourceTestBase.php | 7 ++++ 4 files changed, 67 insertions(+), 15 deletions(-) diff --git a/core/modules/rest/src/Tests/EntityResourceTestBase.php b/core/modules/rest/src/Tests/EntityResourceTestBase.php index 9be05ed..35e2925 100644 --- a/core/modules/rest/src/Tests/EntityResourceTestBase.php +++ b/core/modules/rest/src/Tests/EntityResourceTestBase.php @@ -45,6 +45,8 @@ protected function provisionEntityResource() { public function setUp() { parent::setUp(); + $this->serializer = $this->container->get('serializer'); + // Set up a HTTP client that accepts relative URLs. $this->httpClient = $this->container->get('http_client_factory') ->fromOptions(['base_uri' => $this->baseUrl]); @@ -105,7 +107,15 @@ public function testGet() { $this->assertSame(200, $response->getStatusCode()); $this->assertSame([static::$mimeType], $response->getHeader('Content-Type')); $this->assertSame(['HIT'], $response->getHeader('X-Drupal-Cache')); - $this->assertEquals($this->getExpectedNormalizedEntity(), Json::decode((string)$response->getBody())); + // Comparing the exact serialization is pointless, because the order of + // fields does not matter (at least not yet). That's why we only compare the + // normalized entity with the decoded response: it's comparing PHP arrays + // instead of strings. + $this->assertEquals($this->getExpectedNormalizedEntity(), $this->serializer->decode((string)$response->getBody(), static::$format)); + // Not only assert the normalization, also assert deserialization of the + // response results in the expected object. + $unserialized = $this->serializer->deserialize((string)$response->getBody(), get_class($this->entity), static::$format); + $this->assertSame($unserialized->uuid(), $this->entity->uuid()); $get_headers = $response->getHeaders(); // Verify that the GET and HEAD responses are the same, that the only diff --git a/core/modules/rest/src/Tests/NodeHalJsonBasicAuthTest.php b/core/modules/rest/src/Tests/NodeHalJsonBasicAuthTest.php index 2679ad1..0f83351 100644 --- a/core/modules/rest/src/Tests/NodeHalJsonBasicAuthTest.php +++ b/core/modules/rest/src/Tests/NodeHalJsonBasicAuthTest.php @@ -2,6 +2,8 @@ namespace Drupal\rest\Tests; +use Drupal\Core\Field\EntityReferenceFieldItemListInterface; +use Drupal\Core\Field\FieldItemListInterface; use Drupal\user\Entity\User; /** @@ -38,8 +40,26 @@ class NodeHalJsonBasicAuthTest extends NodeResourceTestBase { * {@inheritdoc} */ protected function getExpectedNormalizedEntity() { + $default_normalization = parent::getExpectedNormalizedEntity(); + + $normalization = $default_normalization; + + // In the HAL normalization, all translatable fields get a 'lang' attribute. + $translatable_non_reference_fields = array_keys(array_filter($this->entity->getTranslatableFields(), function (FieldItemListInterface $field) { + return !$field instanceof EntityReferenceFieldItemListInterface; + })); + foreach ($translatable_non_reference_fields as $field_name) { + $normalized_field = &$normalization[$field_name]; + $normalized_field[0]['lang'] = 'en'; + } + + // In the HAL normalization, 'type' omits the 'target_type' and + // 'target_uuid' properties, because it's encoded in the '_links' section. + unset($normalization['type'][0]['target_type']); + unset($normalization['type'][0]['target_uuid']); + $author = User::load(0); - return parent::getExpectedNormalizedEntity() + [ + return $normalization + [ '_links' => [ 'self' => [ 'href' => $this->baseUrl . 'node/1?_format=hal_json', diff --git a/core/modules/rest/src/Tests/NodeResourceTestBase.php b/core/modules/rest/src/Tests/NodeResourceTestBase.php index a5880c1..0bed097 100644 --- a/core/modules/rest/src/Tests/NodeResourceTestBase.php +++ b/core/modules/rest/src/Tests/NodeResourceTestBase.php @@ -4,6 +4,7 @@ use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; +use Drupal\user\Entity\User; /** * @group rest @@ -44,6 +45,7 @@ protected function createEntity() { protected function getExpectedNormalizedEntity() { assert('$this->entity instanceof \Drupal\Core\Entity\EntityInterface', 'Entity must already have been created.'); + $author = User::load(0); return [ 'nid' => [ ['value' => 1], @@ -57,65 +59,78 @@ protected function getExpectedNormalizedEntity() { 'langcode' => [ [ 'value' => 'en', - 'lang' => 'en', ], ], 'type' => [ - ['target_id' => 'camelids'], + [ + 'target_id' => 'camelids', + 'target_type' => 'node_type', + 'target_uuid' => NodeType::load('camelids')->uuid(), + ], ], 'title' => [ [ 'value' => 'Llama', - 'lang' => 'en', ], ], 'status' => [ [ 'value' => 1, - 'lang' => 'en', ], ], 'created' => [ [ - 'value' => 123456789, - 'lang' => 'en', + 'value' => '123456789', ], ], 'changed' => [ [ - 'value' => 123456789, - 'lang' => 'en', + 'value' => '123456789', ], ], 'promote' => [ [ 'value' => 1, - 'lang' => 'en', ], ], 'sticky' => [ [ 'value' => '0', - 'lang' => 'en', ], ], 'revision_timestamp' => [ [ - 'value' => 123456789, + 'value' => '123456789', ], ], 'revision_translation_affected' => [ [ 'value' => TRUE, - 'lang' => 'en', ], ], 'default_langcode' => [ [ 'value' => TRUE, - 'lang' => 'en', ], ], + 'uid' => [ + [ + 'target_id' => 0, + 'target_type' => 'user', + 'target_uuid' => $author->uuid(), + 'url' => '/user/0', + ], + ], + 'revision_uid' => [ + [ + 'target_id' => 0, + 'target_type' => 'user', + 'target_uuid' => $author->uuid(), + 'url' => '/user/0', + ], + ], + 'revision_log' => [ + ], ]; } diff --git a/core/modules/rest/src/Tests/ResourceTestBase.php b/core/modules/rest/src/Tests/ResourceTestBase.php index 19b1c81..fe8e53b 100644 --- a/core/modules/rest/src/Tests/ResourceTestBase.php +++ b/core/modules/rest/src/Tests/ResourceTestBase.php @@ -63,6 +63,13 @@ class ResourceTestBase extends BrowserTestBase { */ protected $resourceConfigStorage; + /** + * The serializer service. + * + * @var \Symfony\Component\Serializer\Serializer + */ + protected $serializer; + /** * Modules to install. * -- 2.9.0 From 19c6468360e06f35e951070ad4b392afad59ae8d Mon Sep 17 00:00:00 2001 From: Wim Leers Date: Thu, 15 Sep 2016 19:54:15 +0200 Subject: [PATCH 5/6] actually make it work --- .../rest/src/Tests/EntityResourceTestBase.php | 9 +++++-- .../rest/src/Tests/NodeHalJsonBasicAuthTest.php | 31 ++++++++++++++++++---- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/core/modules/rest/src/Tests/EntityResourceTestBase.php b/core/modules/rest/src/Tests/EntityResourceTestBase.php index 35e2925..77855dc 100644 --- a/core/modules/rest/src/Tests/EntityResourceTestBase.php +++ b/core/modules/rest/src/Tests/EntityResourceTestBase.php @@ -166,8 +166,13 @@ public function testGet() { $this->assertSame(404, $response->getStatusCode()); $this->assertSame([static::$expectedErrorMimeType], $response->getHeader('Content-Type')); $path = str_replace('987654321', '{' . static::$entityType . '}', $non_existing_entity_url->setOption('query', [])->toString()); - $expected_message = Json::encode(['message' => 'The "' . static::$entityType . '" parameter was not converted for the path "' . $path . '" (route name: "rest.entity.' . static::$entityType . '.GET.' . static::$format . '")']); - $this->assertSame($expected_message, (string)$response->getBody()); + + $message = ['message' => 'The "' . static::$entityType . '" parameter was not converted for the path "' . $path . '" (route name: "rest.entity.' . static::$entityType . '.GET.' . static::$format . '")']; + // @todo Either add this to \Drupal\serialization\Encoder\JsonEncoder, or + // figure out how to let tests specify encoder options, and figure out + // whether they should apply to just error responses or to everything. + $encode_options = ['json_encode_options' => JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT]; + $this->assertSame($this->serializer->encode($message, static::$format, $encode_options), (string)$response->getBody()); } } diff --git a/core/modules/rest/src/Tests/NodeHalJsonBasicAuthTest.php b/core/modules/rest/src/Tests/NodeHalJsonBasicAuthTest.php index 0f83351..b851be2 100644 --- a/core/modules/rest/src/Tests/NodeHalJsonBasicAuthTest.php +++ b/core/modules/rest/src/Tests/NodeHalJsonBasicAuthTest.php @@ -49,14 +49,35 @@ protected function getExpectedNormalizedEntity() { return !$field instanceof EntityReferenceFieldItemListInterface; })); foreach ($translatable_non_reference_fields as $field_name) { - $normalized_field = &$normalization[$field_name]; - $normalized_field[0]['lang'] = 'en'; + $normalization[$field_name][0]['lang'] = 'en'; } - // In the HAL normalization, 'type' omits the 'target_type' and + // In the HAL normalization, reference fields are omitted, except for the + // bundle field. + $bundle_key = $this->entity->getEntityType()->getKey('bundle'); + $reference_fields = array_keys(array_filter($this->entity->getFields(), function (FieldItemListInterface $field) use ($bundle_key) { + if ($field->getName() === $bundle_key) { + return FALSE; + } + return $field instanceof EntityReferenceFieldItemListInterface; + })); + foreach ($reference_fields as $field_name) { + unset($normalization[$field_name]); + } + + // In the HAL normalization, the bundle field omits the 'target_type' and // 'target_uuid' properties, because it's encoded in the '_links' section. - unset($normalization['type'][0]['target_type']); - unset($normalization['type'][0]['target_uuid']); + if ($bundle_key) { + unset($normalization[$bundle_key][0]['target_type']); + unset($normalization[$bundle_key][0]['target_uuid']); + } + + // In the HAL normalization, empty fields are omitted. + foreach ($normalization as $field_name => $data) { + if (empty($normalization[$field_name])) { + unset($normalization[$field_name]); + } + } $author = User::load(0); return $normalization + [ -- 2.9.0 From 0b7db14fd14bf41257ce006903d12f476a0d6a25 Mon Sep 17 00:00:00 2001 From: Wim Leers Date: Thu, 15 Sep 2016 20:02:25 +0200 Subject: [PATCH 6/6] xml --- .../rest/src/Tests/NodeXmlBasicAuthTest.php | 43 ++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 core/modules/rest/src/Tests/NodeXmlBasicAuthTest.php diff --git a/core/modules/rest/src/Tests/NodeXmlBasicAuthTest.php b/core/modules/rest/src/Tests/NodeXmlBasicAuthTest.php new file mode 100644 index 0000000..5e2df00 --- /dev/null +++ b/core/modules/rest/src/Tests/NodeXmlBasicAuthTest.php @@ -0,0 +1,43 @@ +