.../tests/src/Functional/AnonResourceTestTrait.php | 28 +++++ .../src/Functional/BasicAuthResourceTestTrait.php | 30 +++++ .../EntityResource/Block/BlockHalJsonAnonTest.php | 41 +++++++ .../Block/BlockHalJsonBasicAuthTest.php | 17 +++ .../EntityResource/Block/BlockJsonAnonTest.php | 36 ++++++ .../Block/BlockJsonBasicAuthTest.php | 17 +++ .../EntityResource/Block/BlockResourceTestBase.php | 43 +++++-- .../EntityResource/EntityResourceTestBase.php | 130 ++++++++++++++++----- .../EntityResource/Node/NodeHalJsonAnonTest.php | 110 +++++++++++++++++ .../Node/NodeHalJsonBasicAuthTest.php | 92 ++------------- .../EntityResource/Node/NodeJsonAnonTest.php | 36 ++++++ .../EntityResource/Node/NodeJsonBasicAuthTest.php | 18 ++- .../EntityResource/Role/RoleHalJsonAnonTest.php | 41 +++++++ .../Role/RoleHalJsonBasicAuthTest.php | 32 +++++ .../EntityResource/Role/RoleJsonAnonTest.php | 36 ++++++ .../EntityResource/Role/RoleJsonBasicAuthTest.php | 35 ++++++ .../EntityResource/Role/RoleResourceTestBase.php | 6 - .../EntityResource/Term/TermHalJsonAnonTest.php | 63 ++++++++++ .../Term/TermHalJsonBasicAuthTest.php | 27 ++--- .../EntityResource/Term/TermJsonAnonTest.php | 36 ++++++ .../EntityResource/Term/TermJsonBasicAuthTest.php | 17 +++ .../HalJsonBasicAuthWorkaroundFor2805281Trait.php | 23 ++++ .../JsonBasicAuthWorkaroundFor2805281Trait.php | 25 ++++ .../rest/tests/src/Functional/ResourceTestBase.php | 58 ++++++++- 24 files changed, 857 insertions(+), 140 deletions(-) diff --git a/core/modules/rest/tests/src/Functional/AnonResourceTestTrait.php b/core/modules/rest/tests/src/Functional/AnonResourceTestTrait.php new file mode 100644 index 0000000..b8b04b1 --- /dev/null +++ b/core/modules/rest/tests/src/Functional/AnonResourceTestTrait.php @@ -0,0 +1,28 @@ +grantPermissions(Role::load(RoleInterface::ANONYMOUS_ID), $permissions); + } + + /** + * {@inheritdoc} + */ + protected function verifyResponseWhenMissingAuthentication(ResponseInterface $response) { + throw new \LogicException('When testing for anonymous users, authentication cannot be missing.'); + } + +} diff --git a/core/modules/rest/tests/src/Functional/BasicAuthResourceTestTrait.php b/core/modules/rest/tests/src/Functional/BasicAuthResourceTestTrait.php new file mode 100644 index 0000000..bb1c997 --- /dev/null +++ b/core/modules/rest/tests/src/Functional/BasicAuthResourceTestTrait.php @@ -0,0 +1,30 @@ + [ + 'Authorization' => 'Basic ' . base64_encode($this->account->name->value . ':' . $this->account->passRaw), + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function verifyResponseWhenMissingAuthentication(ResponseInterface $response) { + $this->assertErrorResponse(401, 'No authentication credentials provided.', $response); + } + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockHalJsonAnonTest.php b/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockHalJsonAnonTest.php new file mode 100644 index 0000000..80b10aa --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockHalJsonAnonTest.php @@ -0,0 +1,41 @@ +grantPermissionsToAnonymousRole(['administer blocks']); + } + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockHalJsonBasicAuthTest.php b/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockHalJsonBasicAuthTest.php index 25512fa..6b613ab 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockHalJsonBasicAuthTest.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockHalJsonBasicAuthTest.php @@ -2,11 +2,16 @@ namespace Drupal\Tests\rest\Functional\EntityResource\Block; +use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait; +use Drupal\Tests\rest\Functional\HalJsonBasicAuthWorkaroundFor2805281Trait; + /** * @group rest */ class BlockHalJsonBasicAuthTest extends BlockResourceTestBase { + use BasicAuthResourceTestTrait; + /** * {@inheritdoc} */ @@ -32,4 +37,16 @@ class BlockHalJsonBasicAuthTest extends BlockResourceTestBase { */ protected static $auth = 'basic_auth'; + /** + * {@inheritdoc} + */ + protected function setUpAuthentication() { + $this->account = $this->createUser(['administer blocks']); + } + + // @todo Fix in https://www.drupal.org/node/2805281: remove this trait usage. + use HalJsonBasicAuthWorkaroundFor2805281Trait { + HalJsonBasicAuthWorkaroundFor2805281Trait::verifyResponseWhenMissingAuthentication insteadof BasicAuthResourceTestTrait; + } + } diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockJsonAnonTest.php b/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockJsonAnonTest.php new file mode 100644 index 0000000..ebfe7ac --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockJsonAnonTest.php @@ -0,0 +1,36 @@ +grantPermissionsToAnonymousRole(['administer blocks']); + } + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockJsonBasicAuthTest.php b/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockJsonBasicAuthTest.php index 71a5e81..c5b803e 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockJsonBasicAuthTest.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockJsonBasicAuthTest.php @@ -2,11 +2,16 @@ namespace Drupal\Tests\rest\Functional\EntityResource\Block; +use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait; +use Drupal\Tests\rest\Functional\JsonBasicAuthWorkaroundFor2805281Trait; + /** * @group rest */ class BlockJsonBasicAuthTest extends BlockResourceTestBase { + use BasicAuthResourceTestTrait; + /** * {@inheritdoc} */ @@ -32,4 +37,16 @@ class BlockJsonBasicAuthTest extends BlockResourceTestBase { */ protected static $auth = 'basic_auth'; + /** + * {@inheritdoc} + */ + protected function setUpAuthentication() { + $this->account = $this->createUser(['administer blocks']); + } + + // @todo Fix in https://www.drupal.org/node/2805281: remove this trait usage. + use JsonBasicAuthWorkaroundFor2805281Trait { + JsonBasicAuthWorkaroundFor2805281Trait::verifyResponseWhenMissingAuthentication insteadof BasicAuthResourceTestTrait; + } + } 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 5f76780..9430a35 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockResourceTestBase.php @@ -29,20 +29,34 @@ protected function createEntity() { 'id' => 'llama', 'theme' => 'classy', ]); + // All blocks can be viewed by the anonymous user by default. An interesting + // side effect of this is that any anonymous user is also able to read the + // corresponding block config entity via REST, even if an authentication + // provider is configured for the block config entity REST resource! In + // other words: Block entities do not distinguish between 'view' as in + // "render on a page" and 'view' as in "read the configuration". + // This prevents that. + // @todo Investigate further. + if ($this->account) { + $block->setVisibilityConfig('user_role', [ + 'id' => 'user_role', + 'roles' => [ + 'authenticated' => 'authenticated', + ], + 'negate' => FALSE, + 'context_mapping' => [ + 'user' => '@user.current_user_context:current_user', + ], + ]); + } $block->save(); - // Give anonymous users permission to view comments, so that we can verify - // the cache tags of cached versions of comment pages. - $user_role = Role::load(RoleInterface::ANONYMOUS_ID); - $user_role->grantPermission('administer blocks'); - $user_role->save(); - return $block; } protected function getExpectedNormalizedEntity() { assert('$this->entity instanceof \Drupal\Core\Entity\EntityInterface', 'Entity must already have been created.'); - return [ + $normalization = [ 'uuid' => $this->entity->uuid(), 'id' => 'llama', 'weight' => NULL, @@ -65,6 +79,21 @@ protected function getExpectedNormalizedEntity() { ], 'visibility' => [], ]; + if ($this->account) { + $normalization['dependencies']['module'][] = 'user'; + $normalization['visibility']['user_role'] = [ + 'id' => 'user_role', + 'roles' => [ + 'authenticated' => 'authenticated', + ], + 'negate' => FALSE, + 'context_mapping' => [ + 'user' => '@user.current_user_context:current_user', + ], + ]; + } + + return $normalization; } } diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php index 29632a7..9ada181 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php @@ -7,6 +7,7 @@ use Drupal\Core\Url; use Drupal\Tests\rest\Functional\ResourceTestBase; use GuzzleHttp\Exception\ClientException; +use Psr\Http\Message\ResponseInterface; /** * Even though there is the generic EntityResource, it's necessary for every @@ -40,7 +41,10 @@ protected function provisionEntityResource() { - $this->provisionResource('entity.' . static::$entityType, [static::$format], [static::$auth]); + // 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); } public function setUp() { @@ -91,33 +95,44 @@ public function testGet() { // Requesting without the appropriate ?_format query argument. try { - $response = $this->httpClient->get($url->toString()); + $response = $this->httpClient->get($url->toString(), $this->getAuthenticationRequestOptions()); } catch (ClientException $e) { - if (!$has_canonical_url) { - $response = $e->getResponse(); - $this->assertSame(406, $response->getStatusCode()); - } + $response = $e->getResponse(); } - if ($has_canonical_url) { + // When the anonymous user accesses a canonical (HTML) route, we won't get a + // 406, but a 200, and that is appropriate when no format is specified. + if ($has_canonical_url && !$this->account) { $this->assertSame(200, $response->getStatusCode()); } - $this->assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type')); + else { + $this->assert406Response($response); + } $url->setOption('query', ['_format' => static::$format]); // Verify that the entity exists: use a HEAD request. - $response = $this->httpClient->head($url->toString()); + $response = $this->httpClient->head($url->toString(), $this->getAuthenticationRequestOptions()); $this->assertSame(200, $response->getStatusCode()); - $this->assertSame(['MISS'], $response->getHeader('X-Drupal-Cache')); + if (!$this->account) { + $this->assertSame(['MISS'], $response->getHeader('X-Drupal-Cache')); + } + else { + $this->assertFalse($response->hasHeader('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 // should be a Page Cache hit. - $response = $this->httpClient->get($url->toString()); + $response = $this->httpClient->get($url->toString(), $this->getAuthenticationRequestOptions()); $this->assertSame(200, $response->getStatusCode()); $this->assertSame([static::$mimeType], $response->getHeader('Content-Type')); - $this->assertSame(['HIT'], $response->getHeader('X-Drupal-Cache')); + if (!$this->account) { + $this->assertSame(['HIT'], $response->getHeader('X-Drupal-Cache')); + } + else { + $this->assertFalse($response->hasHeader('X-Drupal-Cache')); + } // 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 @@ -131,37 +146,50 @@ public function testGet() { // 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']; + $ignored_headers = ['Date', 'Content-Length', 'X-Drupal-Cache', 'X-Drupal-Dynamic-Cache']; foreach ($ignored_headers as $ignored_header) { unset($head_headers[$ignored_header]); unset($get_headers[$ignored_header]); } $this->assertSame($get_headers, $head_headers); + if ($this->account) { + // Try to read the entity without the necessary authentication. + try { + $this->httpClient->get($url->toString()); + $this->fail('No error response for missing authentication!'); + } + catch (ClientException $e) { + $this->verifyResponseWhenMissingAuthentication($e->getResponse()); + } + } + // Try to read the entity in an unsupported format. try { $url->setOption('query', ['_format' => 'non_existing_format']); - $this->httpClient->get($url->toString()); + $this->httpClient->get($url->toString(), $this->getAuthenticationRequestOptions()); $this->fail('No 406 response received.'); } catch (ClientException $e) { $response = $e->getResponse(); - $this->assertSame(406, $response->getStatusCode()); + $this->assert406Response($response); // @todo this works fine locally, but on testbot it comes back with 'text/plain; charset=UTF-8'. WTF. // $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. + // Accept header. This time we should get a response with the expected error + // MIME type. try { - $headers = ['Accept' => static::$mimeType]; + $options = $this->getAuthenticationRequestOptions(); + $options['headers']['Accept'] = static::$mimeType; $url->setOption('query', ['_format' => 'non_existing_format']); - $this->httpClient->get($url->toString(), ['headers' => $headers]); + $this->httpClient->get($url->toString(), $options); $this->fail('No 406 response received.'); } catch (ClientException $e) { $response = $e->getResponse(); - $this->assertSame(406, $response->getStatusCode()); + $this->assert406Response($response); $this->assertSame([static::$expectedErrorMimeType], $response->getHeader('Content-Type')); } @@ -174,19 +202,36 @@ public function testGet() { $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->setAbsolute()->setOptions(['base_url' => '', 'query' => []])->toString()); - $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()); + $message = 'The "' . static::$entityType . '" parameter was not converted for the path "' . $path . '" (route name: "rest.entity.' . static::$entityType . '.GET.' . static::$format . '")'; + $this->assertErrorResponse(404, $message, $e->getResponse()); } } + /** + * Asserts that an error response has the given status code and message. + * + * (Also asserts that the expected error MIME type is present, but this is + * defined globally for the test via static::$expectedErrorMimeType, because + * all error responses should use the same MIME type.) + * + * @param int $expected_status_code + * The expected response status. + * @param string $expected_message + * The expected error message. + * @param \Psr\Http\Message\ResponseInterface $response + * The error response to assert. + */ + protected function assertErrorResponse($expected_status_code, $expected_message, ResponseInterface $response) { + $this->assertSame($expected_status_code, $response->getStatusCode()); + $this->assertSame([static::$expectedErrorMimeType], $response->getHeader('Content-Type')); + // @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' => $expected_message], static::$format, $encode_options), (string)$response->getBody()); + } + // @todo try the following request bodies: // 1. empty (only makes sense for DELETE and GET), assert 400 // 2. not parseable, assert 400 @@ -212,6 +257,35 @@ public function atestPost() { $this->assertSession()->responseContains('…'); } + /** + * Asserts a 406 response… or in some cases a 403 response, because weirdness. + * + * Asserting a 406 response should be easy, but it's not, due to bugs. + * + * Drupal returns a 403 response instead of a 406 response when: + * - there is a canonical route, i.e. one that serves HTML + * - unless the user is logged in with any non-global authentication provider, + * because then they tried to access a route that requires the user to be + * authenticated, but they used an authentication provider that is only + * accepted for specific routes, and HTML routes never have such specific + * authentication providers specified. (By default, only 'cookie' is a + * global authentication provider.) + * + * @todo Remove this in https://www.drupal.org/node/2805279. + * + * @param \Psr\Http\Message\ResponseInterface $response + * The response to assert. + */ + protected function assert406Response(ResponseInterface $response) { + if ($this->entity->hasLinkTemplate('canonical') && ($this->account && static::$auth !== 'cookie')) { + $this->assertSame(403, $response->getStatusCode()); + } + else { + // This is the desired response. + $this->assertSame(406, $response->getStatusCode()); + } + } + /** * Simulate common developer mistake when performing an unsafe operation: * - forget to specify the X-CSRF-Token request header diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php new file mode 100644 index 0000000..34b45a7 --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php @@ -0,0 +1,110 @@ +grantPermissionsToAnonymousRole(['access content']); + } + + /** + * {@inheritdoc} + */ + protected function getExpectedNormalizedEntity() { + $default_normalization = parent::getExpectedNormalizedEntity(); + + $normalization = $this->applyHalFieldNormalization($default_normalization); + + $author = User::load(0); + return $normalization + [ + '_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()] + ], + ], + ], + ], + ]; + } + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeHalJsonBasicAuthTest.php b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeHalJsonBasicAuthTest.php index 9b68851..a6260bb 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeHalJsonBasicAuthTest.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeHalJsonBasicAuthTest.php @@ -2,35 +2,20 @@ namespace Drupal\Tests\rest\Functional\EntityResource\Node; -use Drupal\Tests\rest\Functional\EntityResource\HalEntityNormalizationTrait; -use Drupal\user\Entity\User; +use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait; +use Drupal\Tests\rest\Functional\HalJsonBasicAuthWorkaroundFor2805281Trait; /** * @group rest */ -class NodeHalJsonBasicAuthTest extends NodeResourceTestBase { +class NodeHalJsonBasicAuthTest extends NodeHalJsonAnonTest { - use HalEntityNormalizationTrait; + use BasicAuthResourceTestTrait; /** * {@inheritdoc} */ - public static $modules = ['hal', 'basic_auth']; - - /** - * {@inheritdoc} - */ - protected static $format = 'hal_json'; - - /** - * {@inheritdoc} - */ - protected static $mimeType = 'application/hal+json'; - - /** - * {@inheritdoc} - */ - protected static $expectedErrorMimeType = 'application/json'; + public static $modules = ['basic_auth']; /** * {@inheritdoc} @@ -40,66 +25,13 @@ class NodeHalJsonBasicAuthTest extends NodeResourceTestBase { /** * {@inheritdoc} */ - protected function getExpectedNormalizedEntity() { - $default_normalization = parent::getExpectedNormalizedEntity(); - - $normalization = $this->applyHalFieldNormalization($default_normalization); + protected function setUpAuthentication() { + $this->account = $this->createUser(['access content']); + } - $author = User::load(0); - return $normalization + [ - '_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()] - ], - ], - ], - ], - ]; + // @todo Fix in https://www.drupal.org/node/2805281: remove this trait usage. + use HalJsonBasicAuthWorkaroundFor2805281Trait { + HalJsonBasicAuthWorkaroundFor2805281Trait::verifyResponseWhenMissingAuthentication insteadof BasicAuthResourceTestTrait; } - // methods to provide one-off expectations/data/… + } diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeJsonAnonTest.php b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeJsonAnonTest.php new file mode 100644 index 0000000..596dd1c --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeJsonAnonTest.php @@ -0,0 +1,36 @@ +grantPermissionsToAnonymousRole(['access content']); + } + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeJsonBasicAuthTest.php b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeJsonBasicAuthTest.php index f4a4dcc..d293972 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeJsonBasicAuthTest.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeJsonBasicAuthTest.php @@ -2,11 +2,17 @@ namespace Drupal\Tests\rest\Functional\EntityResource\Node; +use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait; +use Drupal\Tests\rest\Functional\JsonBasicAuthWorkaroundFor2805281Trait; +use Psr\Http\Message\ResponseInterface; + /** * @group rest */ class NodeJsonBasicAuthTest extends NodeResourceTestBase { + use BasicAuthResourceTestTrait; + /** * {@inheritdoc} */ @@ -32,6 +38,16 @@ class NodeJsonBasicAuthTest extends NodeResourceTestBase { */ protected static $auth = 'basic_auth'; + /** + * {@inheritdoc} + */ + protected function setUpAuthentication() { + $this->account = $this->createUser(['access content']); + } + + // @todo Fix in https://www.drupal.org/node/2805281: remove this trait usage. + use JsonBasicAuthWorkaroundFor2805281Trait { + JsonBasicAuthWorkaroundFor2805281Trait::verifyResponseWhenMissingAuthentication insteadof BasicAuthResourceTestTrait; + } - // methods to provide one-off expectations/data/… } diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Role/RoleHalJsonAnonTest.php b/core/modules/rest/tests/src/Functional/EntityResource/Role/RoleHalJsonAnonTest.php new file mode 100644 index 0000000..d8535d2 --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/Role/RoleHalJsonAnonTest.php @@ -0,0 +1,41 @@ +grantPermissionsToAnonymousRole(['administer permissions']); + } + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Role/RoleHalJsonBasicAuthTest.php b/core/modules/rest/tests/src/Functional/EntityResource/Role/RoleHalJsonBasicAuthTest.php index 9021546..64e3fef 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Role/RoleHalJsonBasicAuthTest.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Role/RoleHalJsonBasicAuthTest.php @@ -2,11 +2,16 @@ namespace Drupal\Tests\rest\Functional\EntityResource\Role; +use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait; +use Drupal\Tests\rest\Functional\HalJsonBasicAuthWorkaroundFor2805281Trait; + /** * @group rest */ class RoleHalJsonBasicAuthTest extends RoleResourceTestBase { + use BasicAuthResourceTestTrait; + /** * {@inheritdoc} */ @@ -32,4 +37,31 @@ class RoleHalJsonBasicAuthTest extends RoleResourceTestBase { */ protected static $auth = 'basic_auth'; + /** + * {@inheritdoc} + */ + protected function setUpAuthentication() { + $this->account = $this->createUser(['administer permissions']); + } + + /** + * {@inheritdoc} + */ + protected function getExpectedNormalizedEntity() { + $default_normalization = parent::getExpectedNormalizedEntity(); + return [ + // Edge case: the weight is not 2 like in the default normalization, but 3 + // because setUpAuthentication() creates a new user role (which gets + // weight 2), and then the entity that is created for testing ends up with + // weight 3. + // This only affects user_role entity tests for authenticated users. + 'weight' => 3, + ] + $default_normalization; + } + + // @todo Fix in https://www.drupal.org/node/2805281: remove this trait usage. + use HalJsonBasicAuthWorkaroundFor2805281Trait { + HalJsonBasicAuthWorkaroundFor2805281Trait::verifyResponseWhenMissingAuthentication insteadof BasicAuthResourceTestTrait; + } + } diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Role/RoleJsonAnonTest.php b/core/modules/rest/tests/src/Functional/EntityResource/Role/RoleJsonAnonTest.php new file mode 100644 index 0000000..103a208 --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/Role/RoleJsonAnonTest.php @@ -0,0 +1,36 @@ +grantPermissionsToAnonymousRole(['administer permissions']); + } + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Role/RoleJsonBasicAuthTest.php b/core/modules/rest/tests/src/Functional/EntityResource/Role/RoleJsonBasicAuthTest.php index 8a94f54..05a0150 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Role/RoleJsonBasicAuthTest.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Role/RoleJsonBasicAuthTest.php @@ -2,11 +2,16 @@ namespace Drupal\Tests\rest\Functional\EntityResource\Role; +use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait; +use Psr\Http\Message\ResponseInterface; + /** * @group rest */ class RoleJsonBasicAuthTest extends RoleResourceTestBase { + use BasicAuthResourceTestTrait; + /** * {@inheritdoc} */ @@ -32,4 +37,34 @@ class RoleJsonBasicAuthTest extends RoleResourceTestBase { */ protected static $auth = 'basic_auth'; + /** + * {@inheritdoc} + */ + protected function setUpAuthentication() { + $this->account = $this->createUser(['administer permissions']); + } + + /** + * {@inheritdoc} + */ + protected function getExpectedNormalizedEntity() { + $default_normalization = parent::getExpectedNormalizedEntity(); + return [ + // Edge case: the weight is not 2 like in the default normalization, but 3 + // because setUpAuthentication() creates a new user role (which gets + // weight 2), and then the entity that is created for testing ends up with + // weight 3. + // This only affects user_role entity tests for authenticated users. + 'weight' => 3, + ] + $default_normalization; + } + + /** + * {@inheritdoc} + */ + protected function verifyResponseWhenMissingAuthentication(ResponseInterface $response) { + $this->assertSame(401, $response->getStatusCode()); + $this->assertSame('{"message":"A fatal error occurred: No authentication credentials provided."}', (string)$response->getBody()); + } + } 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 8e6388c..4255e2d 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Role/RoleResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Role/RoleResourceTestBase.php @@ -28,12 +28,6 @@ protected function createEntity() { ]); $role->save(); - // Give anonymous users permission to view comments, so that we can verify - // the cache tags of cached versions of comment pages. - $user_role = Role::load(RoleInterface::ANONYMOUS_ID); - $user_role->grantPermission('administer permissions'); - $user_role->save(); - return $role; } diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermHalJsonAnonTest.php b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermHalJsonAnonTest.php new file mode 100644 index 0000000..3d0f78f --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermHalJsonAnonTest.php @@ -0,0 +1,63 @@ +grantPermissionsToAnonymousRole(['access content']); + } + + /** + * {@inheritdoc} + */ + protected function getExpectedNormalizedEntity() { + $default_normalization = parent::getExpectedNormalizedEntity(); + + $normalization = $this->applyHalFieldNormalization($default_normalization); + + return $normalization + [ + '_links' => [ + 'self' => [ + 'href' => $this->baseUrl . '/taxonomy/term/1?_format=hal_json', + ], + 'type' => [ + 'href' => $this->baseUrl . '/rest/type/taxonomy_term/camelids', + ], + ], + ]; + } + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermHalJsonBasicAuthTest.php b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermHalJsonBasicAuthTest.php index 06d36ba..4df21b0 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermHalJsonBasicAuthTest.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermHalJsonBasicAuthTest.php @@ -2,14 +2,17 @@ namespace Drupal\Tests\rest\Functional\EntityResource\Term; +use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait; use Drupal\Tests\rest\Functional\EntityResource\HalEntityNormalizationTrait; +use Drupal\Tests\rest\Functional\HalJsonBasicAuthWorkaroundFor2805281Trait; /** * @group rest */ -class TermHalJsonBasicAuthTest extends TermResourceTestBase { +class TermHalJsonBasicAuthTest extends TermHalJsonAnonTest { use HalEntityNormalizationTrait; + use BasicAuthResourceTestTrait; /** * {@inheritdoc} @@ -39,21 +42,13 @@ class TermHalJsonBasicAuthTest extends TermResourceTestBase { /** * {@inheritdoc} */ - protected function getExpectedNormalizedEntity() { - $default_normalization = parent::getExpectedNormalizedEntity(); - - $normalization = $this->applyHalFieldNormalization($default_normalization); - - return $normalization + [ - '_links' => [ - 'self' => [ - 'href' => $this->baseUrl . '/taxonomy/term/1?_format=hal_json', - ], - 'type' => [ - 'href' => $this->baseUrl . '/rest/type/taxonomy_term/camelids', - ], - ], - ]; + protected function setUpAuthentication() { + $this->account = $this->createUser(['access content']); + } + + // @todo Fix in https://www.drupal.org/node/2805281: remove this trait usage. + use HalJsonBasicAuthWorkaroundFor2805281Trait { + HalJsonBasicAuthWorkaroundFor2805281Trait::verifyResponseWhenMissingAuthentication insteadof BasicAuthResourceTestTrait; } } diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermJsonAnonTest.php b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermJsonAnonTest.php new file mode 100644 index 0000000..60fa865 --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermJsonAnonTest.php @@ -0,0 +1,36 @@ +grantPermissionsToAnonymousRole(['access content']); + } + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermJsonBasicAuthTest.php b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermJsonBasicAuthTest.php index bd352d3..84c5cf2 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermJsonBasicAuthTest.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermJsonBasicAuthTest.php @@ -2,11 +2,16 @@ namespace Drupal\Tests\rest\Functional\EntityResource\Term; +use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait; +use Drupal\Tests\rest\Functional\JsonBasicAuthWorkaroundFor2805281Trait; + /** * @group rest */ class TermJsonBasicAuthTest extends TermResourceTestBase { + use BasicAuthResourceTestTrait; + /** * {@inheritdoc} */ @@ -32,4 +37,16 @@ class TermJsonBasicAuthTest extends TermResourceTestBase { */ protected static $auth = 'basic_auth'; + /** + * {@inheritdoc} + */ + protected function setUpAuthentication() { + $this->account = $this->createUser(['access content']); + } + + // @todo Fix in https://www.drupal.org/node/2805281: remove this trait usage. + use JsonBasicAuthWorkaroundFor2805281Trait { + JsonBasicAuthWorkaroundFor2805281Trait::verifyResponseWhenMissingAuthentication insteadof BasicAuthResourceTestTrait; + } + } diff --git a/core/modules/rest/tests/src/Functional/HalJsonBasicAuthWorkaroundFor2805281Trait.php b/core/modules/rest/tests/src/Functional/HalJsonBasicAuthWorkaroundFor2805281Trait.php new file mode 100644 index 0000000..c635a7f --- /dev/null +++ b/core/modules/rest/tests/src/Functional/HalJsonBasicAuthWorkaroundFor2805281Trait.php @@ -0,0 +1,23 @@ +assertSame(401, $response->getStatusCode()); + $this->assertSame(['application/hal+json'], $response->getHeader('Content-Type')); + $this->assertSame('No authentication credentials provided.', (string)$response->getBody()); + } +} diff --git a/core/modules/rest/tests/src/Functional/JsonBasicAuthWorkaroundFor2805281Trait.php b/core/modules/rest/tests/src/Functional/JsonBasicAuthWorkaroundFor2805281Trait.php new file mode 100644 index 0000000..2d76a71 --- /dev/null +++ b/core/modules/rest/tests/src/Functional/JsonBasicAuthWorkaroundFor2805281Trait.php @@ -0,0 +1,25 @@ +assertSame(401, $response->getStatusCode()); + $this->assertSame([static::$expectedErrorMimeType], $response->getHeader('Content-Type')); + // Note that strange 'A fatal error occurred: ' prefix, that should not + // exist. + // @todo Fix in https://www.drupal.org/node/2805281. + $this->assertSame('{"message":"A fatal error occurred: No authentication credentials provided."}', (string)$response->getBody()); + } + +} diff --git a/core/modules/rest/tests/src/Functional/ResourceTestBase.php b/core/modules/rest/tests/src/Functional/ResourceTestBase.php index 5f178a2..22132c4 100644 --- a/core/modules/rest/tests/src/Functional/ResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/ResourceTestBase.php @@ -4,11 +4,14 @@ use Drupal\rest\RestResourceConfigInterface; use Drupal\Tests\BrowserTestBase; +use Drupal\user\Entity\Role; +use Drupal\user\RoleInterface; +use Psr\Http\Message\ResponseInterface; /** * Subclass this for every REST resource, every format and every auth mechanism. */ -class ResourceTestBase extends BrowserTestBase { +abstract class ResourceTestBase extends BrowserTestBase { /** * The format to use in this test. @@ -54,7 +57,14 @@ class ResourceTestBase extends BrowserTestBase { * * @todo it SHOULD be possible to iterate over all authentication mechanisms and do all of those in a single test? The problem is that we'd then have to enable all modules that provide auth mechanisms. Which can include contrib. So doing a separate test per auth mechanism makes it easier for contrib to add tests. */ - protected static $auth = 'cookie'; + protected static $auth = FALSE; + + /** + * The account to use for authentication, if any. + * + * @var null|\Drupal\Core\Session\AccountInterface + */ + protected $account = NULL; /** * The REST resource config entity storage. @@ -83,6 +93,18 @@ class ResourceTestBase extends BrowserTestBase { public function setUp() { parent::setUp(); + // Ensure the anonymous user has no permissions at all. + $user_role = Role::load(RoleInterface::ANONYMOUS_ID); + foreach ($user_role->getPermissions() as $permission) { + $user_role->revokePermission($permission); + } + $user_role->save(); + assert('[] === $user_role->getPermissions()', 'The anonymous user has no permissions at all.'); + + // Then allow concrete tests to set up authentication: grant permissions to + // the anonymous user, or create a user with the necessary permissions. + $this->setUpAuthentication(); + $this->resourceConfigStorage = $this->container->get('entity_type.manager')->getStorage('rest_resource_config'); // Ensure there's a clean slate: delete all REST resource config entities. @@ -113,4 +135,36 @@ protected function provisionResource($resource_type, $formats = [], $authenticat $this->container->get('router.builder')->rebuild(); } + /** + * Sets up the necessary authentication. + * + * In case of a test verifying publicly accessible REST resources: grant + * permissions to the anonymous user role. + * + * In case of a test verifying behavior when using a particular authentication + * provider: create a user with a particular set of permissions. + * + * @return void + */ + abstract protected function setUpAuthentication(); + + /** + * Verifies the error response in case of missing authentication. + * + * @return void + */ + abstract protected function verifyResponseWhenMissingAuthentication(ResponseInterface $response); + + /** + * Returns Guzzle request options for authentication. + * + * @return array + * Guzzle request options to use for authentication. + * + * @see \GuzzleHttp\ClientInterface::request() + */ + protected function getAuthenticationRequestOptions() { + return []; + } + }