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