Chris@18: NULL, Chris@18: 'created' => "The 'administer nodes' permission is required.", Chris@18: 'changed' => NULL, Chris@18: 'promote' => "The 'administer nodes' permission is required.", Chris@18: 'sticky' => "The 'administer nodes' permission is required.", Chris@18: 'path' => "The following permissions are required: 'create url aliases' OR 'administer url aliases'.", Chris@18: 'revision_uid' => NULL, Chris@18: ]; Chris@18: Chris@18: /** Chris@18: * {@inheritdoc} Chris@18: */ Chris@18: protected function setUpAuthorization($method) { Chris@18: switch ($method) { Chris@18: case 'GET': Chris@18: $this->grantPermissionsToTestedRole(['access content']); Chris@18: break; Chris@18: Chris@18: case 'POST': Chris@18: $this->grantPermissionsToTestedRole(['access content', 'create camelids content']); Chris@18: break; Chris@18: Chris@18: case 'PATCH': Chris@18: // Do not grant the 'create url aliases' permission to test the case Chris@18: // when the path field is protected/not accessible, see Chris@18: // \Drupal\Tests\rest\Functional\EntityResource\Term\TermResourceTestBase Chris@18: // for a positive test. Chris@18: $this->grantPermissionsToTestedRole(['access content', 'edit any camelids content']); Chris@18: break; Chris@18: Chris@18: case 'DELETE': Chris@18: $this->grantPermissionsToTestedRole(['access content', 'delete any camelids content']); Chris@18: break; Chris@18: } Chris@18: } Chris@18: Chris@18: /** Chris@18: * {@inheritdoc} Chris@18: */ Chris@18: protected function setUpRevisionAuthorization($method) { Chris@18: parent::setUpRevisionAuthorization($method); Chris@18: $this->grantPermissionsToTestedRole(['view all revisions']); Chris@18: } Chris@18: Chris@18: /** Chris@18: * {@inheritdoc} Chris@18: */ Chris@18: protected function createEntity() { Chris@18: if (!NodeType::load('camelids')) { Chris@18: // Create a "Camelids" node type. Chris@18: NodeType::create([ Chris@18: 'name' => 'Camelids', Chris@18: 'type' => 'camelids', Chris@18: ])->save(); Chris@18: } Chris@18: Chris@18: // Create a "Llama" node. Chris@18: $node = Node::create(['type' => 'camelids']); Chris@18: $node->setTitle('Llama') Chris@18: ->setOwnerId($this->account->id()) Chris@18: ->setPublished() Chris@18: ->setCreatedTime(123456789) Chris@18: ->setChangedTime(123456789) Chris@18: ->setRevisionCreationTime(123456789) Chris@18: ->set('path', '/llama') Chris@18: ->save(); Chris@18: Chris@18: return $node; Chris@18: } Chris@18: Chris@18: /** Chris@18: * {@inheritdoc} Chris@18: */ Chris@18: protected function getExpectedDocument() { Chris@18: $author = User::load($this->entity->getOwnerId()); Chris@18: $base_url = Url::fromUri('base:/jsonapi/node/camelids/' . $this->entity->uuid())->setAbsolute(); Chris@18: $self_url = clone $base_url; Chris@18: $version_identifier = 'id:' . $this->entity->getRevisionId(); Chris@18: $self_url = $self_url->setOption('query', ['resourceVersion' => $version_identifier]); Chris@18: $version_query_string = '?resourceVersion=' . urlencode($version_identifier); Chris@18: return [ Chris@18: 'jsonapi' => [ Chris@18: 'meta' => [ Chris@18: 'links' => [ Chris@18: 'self' => ['href' => 'http://jsonapi.org/format/1.0/'], Chris@18: ], Chris@18: ], Chris@18: 'version' => '1.0', Chris@18: ], Chris@18: 'links' => [ Chris@18: 'self' => ['href' => $base_url->toString()], Chris@18: ], Chris@18: 'data' => [ Chris@18: 'id' => $this->entity->uuid(), Chris@18: 'type' => 'node--camelids', Chris@18: 'links' => [ Chris@18: 'self' => ['href' => $self_url->toString()], Chris@18: ], Chris@18: 'attributes' => [ Chris@18: 'created' => '1973-11-29T21:33:09+00:00', Chris@18: 'changed' => (new \DateTime())->setTimestamp($this->entity->getChangedTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339), Chris@18: 'default_langcode' => TRUE, Chris@18: 'langcode' => 'en', Chris@18: 'path' => [ Chris@18: 'alias' => '/llama', Chris@18: 'pid' => 1, Chris@18: 'langcode' => 'en', Chris@18: ], Chris@18: 'promote' => TRUE, Chris@18: 'revision_log' => NULL, Chris@18: 'revision_timestamp' => '1973-11-29T21:33:09+00:00', Chris@18: // @todo Attempt to remove this in https://www.drupal.org/project/drupal/issues/2933518. Chris@18: 'revision_translation_affected' => TRUE, Chris@18: 'status' => TRUE, Chris@18: 'sticky' => FALSE, Chris@18: 'title' => 'Llama', Chris@18: 'drupal_internal__nid' => 1, Chris@18: 'drupal_internal__vid' => 1, Chris@18: ], Chris@18: 'relationships' => [ Chris@18: 'node_type' => [ Chris@18: 'data' => [ Chris@18: 'id' => NodeType::load('camelids')->uuid(), Chris@18: 'type' => 'node_type--node_type', Chris@18: ], Chris@18: 'links' => [ Chris@18: 'related' => [ Chris@18: 'href' => $base_url->toString() . '/node_type' . $version_query_string, Chris@18: ], Chris@18: 'self' => [ Chris@18: 'href' => $base_url->toString() . '/relationships/node_type' . $version_query_string, Chris@18: ], Chris@18: ], Chris@18: ], Chris@18: 'uid' => [ Chris@18: 'data' => [ Chris@18: 'id' => $author->uuid(), Chris@18: 'type' => 'user--user', Chris@18: ], Chris@18: 'links' => [ Chris@18: 'related' => [ Chris@18: 'href' => $base_url->toString() . '/uid' . $version_query_string, Chris@18: ], Chris@18: 'self' => [ Chris@18: 'href' => $base_url->toString() . '/relationships/uid' . $version_query_string, Chris@18: ], Chris@18: ], Chris@18: ], Chris@18: 'revision_uid' => [ Chris@18: 'data' => [ Chris@18: 'id' => $author->uuid(), Chris@18: 'type' => 'user--user', Chris@18: ], Chris@18: 'links' => [ Chris@18: 'related' => [ Chris@18: 'href' => $base_url->toString() . '/revision_uid' . $version_query_string, Chris@18: ], Chris@18: 'self' => [ Chris@18: 'href' => $base_url->toString() . '/relationships/revision_uid' . $version_query_string, Chris@18: ], Chris@18: ], Chris@18: ], Chris@18: ], Chris@18: ], Chris@18: ]; Chris@18: } Chris@18: Chris@18: /** Chris@18: * {@inheritdoc} Chris@18: */ Chris@18: protected function getPostDocument() { Chris@18: return [ Chris@18: 'data' => [ Chris@18: 'type' => 'node--camelids', Chris@18: 'attributes' => [ Chris@18: 'title' => 'Dramallama', Chris@18: ], Chris@18: ], Chris@18: ]; Chris@18: } Chris@18: Chris@18: /** Chris@18: * {@inheritdoc} Chris@18: */ Chris@18: protected function getExpectedUnauthorizedAccessMessage($method) { Chris@18: switch ($method) { Chris@18: case 'GET': Chris@18: case 'POST': Chris@18: case 'PATCH': Chris@18: case 'DELETE': Chris@18: return "The 'access content' permission is required."; Chris@18: } Chris@18: } Chris@18: Chris@18: /** Chris@18: * Tests PATCHing a node's path with and without 'create url aliases'. Chris@18: * Chris@18: * For a positive test, see the similar test coverage for Term. Chris@18: * Chris@18: * @see \Drupal\Tests\jsonapi\Functional\TermTest::testPatchPath() Chris@18: * @see \Drupal\Tests\rest\Functional\EntityResource\Term\TermResourceTestBase::testPatchPath() Chris@18: */ Chris@18: public function testPatchPath() { Chris@18: $this->setUpAuthorization('GET'); Chris@18: $this->setUpAuthorization('PATCH'); Chris@18: $this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE); Chris@18: Chris@18: // @todo Remove line below in favor of commented line in https://www.drupal.org/project/jsonapi/issues/2878463. Chris@18: $url = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), ['entity' => $this->entity->uuid()]); Chris@18: /* $url = $this->entity->toUrl('jsonapi'); */ Chris@18: Chris@18: // GET node's current normalization. Chris@18: $response = $this->request('GET', $url, $this->getAuthenticationRequestOptions()); Chris@18: $normalization = Json::decode((string) $response->getBody()); Chris@18: Chris@18: // Change node's path alias. Chris@18: $normalization['data']['attributes']['path']['alias'] .= 's-rule-the-world'; Chris@18: Chris@18: // Create node PATCH request. Chris@18: $request_options = $this->getAuthenticationRequestOptions(); Chris@18: $request_options[RequestOptions::HEADERS]['Content-Type'] = 'application/vnd.api+json'; Chris@18: $request_options[RequestOptions::BODY] = Json::encode($normalization); Chris@18: Chris@18: // PATCH request: 403 when creating URL aliases unauthorized. Chris@18: $response = $this->request('PATCH', $url, $request_options); Chris@18: $this->assertResourceErrorResponse(403, "The current user is not allowed to PATCH the selected field (path). The following permissions are required: 'create url aliases' OR 'administer url aliases'.", $url, $response, '/data/attributes/path'); Chris@18: Chris@18: // Grant permission to create URL aliases. Chris@18: $this->grantPermissionsToTestedRole(['create url aliases']); Chris@18: Chris@18: // Repeat PATCH request: 200. Chris@18: $response = $this->request('PATCH', $url, $request_options); Chris@18: $this->assertResourceResponse(200, FALSE, $response); Chris@18: $updated_normalization = Json::decode((string) $response->getBody()); Chris@18: $this->assertSame($normalization['data']['attributes']['path']['alias'], $updated_normalization['data']['attributes']['path']['alias']); Chris@18: } Chris@18: Chris@18: /** Chris@18: * {@inheritdoc} Chris@18: */ Chris@18: public function testGetIndividual() { Chris@18: parent::testGetIndividual(); Chris@18: Chris@18: // Unpublish node. Chris@18: $this->entity->setUnpublished()->save(); Chris@18: Chris@18: // @todo Remove line below in favor of commented line in https://www.drupal.org/project/jsonapi/issues/2878463. Chris@18: $url = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), ['entity' => $this->entity->uuid()]); Chris@18: /* $url = $this->entity->toUrl('jsonapi'); */ Chris@18: $request_options = $this->getAuthenticationRequestOptions(); Chris@18: Chris@18: // 403 when accessing own unpublished node. Chris@18: $response = $this->request('GET', $url, $request_options); Chris@18: // @todo Remove $expected + assertResourceResponse() in favor of the commented line below once https://www.drupal.org/project/jsonapi/issues/2943176 lands. Chris@18: $expected_document = [ Chris@18: 'jsonapi' => static::$jsonApiMember, Chris@18: 'errors' => [ Chris@18: [ Chris@18: 'title' => 'Forbidden', Chris@18: 'status' => '403', Chris@18: 'detail' => 'The current user is not allowed to GET the selected resource.', Chris@18: 'links' => [ Chris@18: 'info' => ['href' => HttpExceptionNormalizer::getInfoUrl(403)], Chris@18: 'via' => ['href' => $url->setAbsolute()->toString()], Chris@18: ], Chris@18: 'source' => [ Chris@18: 'pointer' => '/data', Chris@18: ], Chris@18: ], Chris@18: ], Chris@18: ]; Chris@18: $this->assertResourceResponse( Chris@18: 403, Chris@18: $expected_document, Chris@18: $response, Chris@18: ['4xx-response', 'http_response', 'node:1'], Chris@18: ['url.query_args:resourceVersion', 'url.site', 'user.permissions'], Chris@18: FALSE, Chris@18: 'MISS' Chris@18: ); Chris@18: /* $this->assertResourceErrorResponse(403, 'The current user is not allowed to GET the selected resource.', $response, '/data'); */ Chris@18: Chris@18: // 200 after granting permission. Chris@18: $this->grantPermissionsToTestedRole(['view own unpublished content']); Chris@18: $response = $this->request('GET', $url, $request_options); Chris@18: // The response varies by 'user', causing the 'user.permissions' cache Chris@18: // context to be optimized away. Chris@18: $expected_cache_contexts = Cache::mergeContexts($this->getExpectedCacheContexts(), ['user']); Chris@18: $expected_cache_contexts = array_diff($expected_cache_contexts, ['user.permissions']); Chris@18: $this->assertResourceResponse(200, FALSE, $response, $this->getExpectedCacheTags(), $expected_cache_contexts, FALSE, 'UNCACHEABLE'); Chris@18: } Chris@18: Chris@18: /** Chris@18: * {@inheritdoc} Chris@18: */ Chris@18: protected static function getIncludePermissions() { Chris@18: return [ Chris@18: 'uid.node_type' => ['administer users'], Chris@18: 'uid.roles' => ['administer permissions'], Chris@18: ]; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Creating relationships to missing resources should be 404 per JSON:API 1.1. Chris@18: * Chris@18: * @see https://github.com/json-api/json-api/issues/1033 Chris@18: */ Chris@18: public function testPostNonExistingAuthor() { Chris@18: $this->setUpAuthorization('POST'); Chris@18: $this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE); Chris@18: $this->grantPermissionsToTestedRole(['administer nodes']); Chris@18: Chris@18: $random_uuid = \Drupal::service('uuid')->generate(); Chris@18: $doc = $this->getPostDocument(); Chris@18: $doc['data']['relationships']['uid']['data'] = [ Chris@18: 'type' => 'user--user', Chris@18: 'id' => $random_uuid, Chris@18: ]; Chris@18: Chris@18: // Create node POST request. Chris@18: $url = Url::fromRoute(sprintf('jsonapi.%s.collection.post', static::$resourceTypeName)); Chris@18: $request_options = $this->getAuthenticationRequestOptions(); Chris@18: $request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json'; Chris@18: $request_options[RequestOptions::HEADERS]['Content-Type'] = 'application/vnd.api+json'; Chris@18: $request_options[RequestOptions::BODY] = Json::encode($doc); Chris@18: Chris@18: // POST request: 404 when adding relationships to non-existing resources. Chris@18: $response = $this->request('POST', $url, $request_options); Chris@18: $expected_document = [ Chris@18: 'errors' => [ Chris@18: 0 => [ Chris@18: 'status' => '404', Chris@18: 'title' => 'Not Found', Chris@18: 'detail' => "The resource identified by `user--user:$random_uuid` (given as a relationship item) could not be found.", Chris@18: 'links' => [ Chris@18: 'info' => ['href' => HttpExceptionNormalizer::getInfoUrl(404)], Chris@18: 'via' => ['href' => $url->setAbsolute()->toString()], Chris@18: ], Chris@18: ], Chris@18: ], Chris@18: 'jsonapi' => static::$jsonApiMember, Chris@18: ]; Chris@18: $this->assertResourceResponse(404, $expected_document, $response); Chris@18: } Chris@18: Chris@18: /** Chris@18: * {@inheritdoc} Chris@18: */ Chris@18: public function testCollectionFilterAccess() { Chris@18: $label_field_name = 'title'; Chris@18: $this->doTestCollectionFilterAccessForPublishableEntities($label_field_name, 'access content', 'bypass node access'); Chris@18: Chris@18: $collection_url = Url::fromRoute('jsonapi.entity_test--bar.collection'); Chris@18: $collection_filter_url = $collection_url->setOption('query', ["filter[spotlight.$label_field_name]" => $this->entity->label()]); Chris@18: $request_options = []; Chris@18: $request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json'; Chris@18: $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions()); Chris@18: Chris@18: $this->revokePermissionsFromTestedRole(['bypass node access']); Chris@18: Chris@18: // 0 results because the node is unpublished. Chris@18: $response = $this->request('GET', $collection_filter_url, $request_options); Chris@18: $doc = Json::decode((string) $response->getBody()); Chris@18: $this->assertCount(0, $doc['data']); Chris@18: Chris@18: $this->grantPermissionsToTestedRole(['view own unpublished content']); Chris@18: Chris@18: // 1 result because the current user is the owner of the unpublished node. Chris@18: $response = $this->request('GET', $collection_filter_url, $request_options); Chris@18: $doc = Json::decode((string) $response->getBody()); Chris@18: $this->assertCount(1, $doc['data']); Chris@18: Chris@18: $this->entity->setOwnerId(0)->save(); Chris@18: Chris@18: // 0 results because the current user is no longer the owner. Chris@18: $response = $this->request('GET', $collection_filter_url, $request_options); Chris@18: $doc = Json::decode((string) $response->getBody()); Chris@18: $this->assertCount(0, $doc['data']); Chris@18: Chris@18: // Assert bubbling of cacheability from query alter hook. Chris@18: $this->assertTrue($this->container->get('module_installer')->install(['node_access_test'], TRUE), 'Installed modules.'); Chris@18: node_access_rebuild(); Chris@18: $this->rebuildAll(); Chris@18: $response = $this->request('GET', $collection_filter_url, $request_options); Chris@18: $this->assertTrue(in_array('user.node_grants:view', explode(' ', $response->getHeader('X-Drupal-Cache-Contexts')[0]), TRUE)); Chris@18: } Chris@18: Chris@18: }