View source
<?php
namespace Drupal\Tests\jsonapi\Functional;
use Behat\Mink\Driver\BrowserKitDriver;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\Random;
use Drupal\Core\Access\AccessResultReasonInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Cache\CacheableResponseInterface;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\ContentEntityNullStorage;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\BooleanItem;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TypedData\DataReferenceTargetDefinition;
use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper;
use Drupal\Core\Url;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\jsonapi\Normalizer\HttpExceptionNormalizer;
use Drupal\jsonapi\Resource\JsonApiDocumentTopLevel;
use Drupal\jsonapi\ResourceResponse;
use Drupal\path\Plugin\Field\FieldType\PathItem;
use Drupal\Tests\BrowserTestBase;
use Drupal\user\Entity\Role;
use Drupal\user\EntityOwnerInterface;
use Drupal\user\RoleInterface;
use Drupal\user\UserInterface;
use GuzzleHttp\RequestOptions;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
abstract class ResourceTestBase extends BrowserTestBase {
use ResourceResponseTestTrait;
protected static $modules = [
'jsonapi',
'basic_auth',
'rest_test',
'jsonapi_test_field_access',
'text',
];
protected static $entityTypeId = NULL;
protected static $resourceTypeName = NULL;
protected static $patchProtectedFieldNames;
protected static $uniqueFieldNames = [];
protected static $firstCreatedEntityId = 2;
protected static $secondCreatedEntityId = 3;
protected static $labelFieldName = NULL;
protected $entity;
protected $anotherEntity;
protected $account;
protected $entityStorage;
protected $serializer;
public function setUp() {
parent::setUp();
$this->serializer = $this->container
->get('jsonapi.serializer_do_not_use_removal_imminent');
$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 role has no permissions at all.');
$user_role = Role::load(RoleInterface::AUTHENTICATED_ID);
foreach ($user_role
->getPermissions() as $permission) {
$user_role
->revokePermission($permission);
}
$user_role
->save();
assert([] === $user_role
->getPermissions(), 'The authenticated user role has no permissions at all.');
$this->account = $this
->createUser();
$this->container
->get('current_user')
->setAccount($this->account);
$this->entityStorage = $this->container
->get('entity_type.manager')
->getStorage(static::$entityTypeId);
$this->entity = $this
->setUpFields($this
->createEntity(), $this->account);
}
protected function setUpFields(EntityInterface $entity, UserInterface $account) {
if (!$entity instanceof FieldableEntityInterface) {
return $entity;
}
$entity_bundle = $entity
->bundle();
$account_bundle = $account
->bundle();
FieldStorageConfig::create([
'entity_type' => static::$entityTypeId,
'field_name' => 'field_rest_test',
'type' => 'text',
])
->setCardinality(1)
->save();
FieldConfig::create([
'entity_type' => static::$entityTypeId,
'field_name' => 'field_rest_test',
'bundle' => $entity_bundle,
])
->setLabel('Test field')
->setTranslatable(FALSE)
->save();
FieldStorageConfig::create([
'entity_type' => static::$entityTypeId,
'field_name' => 'field_jsonapi_test_entity_ref',
'type' => 'entity_reference',
])
->setSetting('target_type', 'user')
->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED)
->save();
FieldConfig::create([
'entity_type' => static::$entityTypeId,
'field_name' => 'field_jsonapi_test_entity_ref',
'bundle' => $entity_bundle,
])
->setTranslatable(FALSE)
->setSetting('handler', 'default')
->setSetting('handler_settings', [
'target_bundles' => NULL,
])
->save();
if (floatval(\Drupal::VERSION) >= 8.5) {
FieldStorageConfig::create([
'entity_type' => static::$entityTypeId,
'field_name' => 'field_rest_test_multivalue',
'type' => 'string',
])
->setCardinality(3)
->save();
FieldConfig::create([
'entity_type' => static::$entityTypeId,
'field_name' => 'field_rest_test_multivalue',
'bundle' => $entity_bundle,
])
->setLabel('Test field: multi-value')
->setTranslatable(FALSE)
->save();
}
\Drupal::service('jsonapi.resource_type.repository')
->clearCachedDefinitions();
\Drupal::service('router.builder')
->rebuild();
$reloaded_entity = $this->entityStorage
->loadUnchanged($entity
->id());
if ($reloaded_entity !== NULL) {
$entity = $reloaded_entity;
$entity
->set('field_rest_test', [
'value' => 'All the faith he had had had had no effect on the outcome of his life.',
]);
$entity
->set('field_jsonapi_test_entity_ref', [
'user' => $account
->id(),
]);
if (floatval(\Drupal::VERSION) >= 8.5) {
$entity
->set('field_rest_test_multivalue', [
[
'value' => 'One',
],
[
'value' => 'Two',
],
]);
}
$entity
->save();
}
return $entity;
}
protected function getEntityCollection() {
if ($this->entityStorage
->getQuery()
->count()
->execute() < 2) {
$this
->createAnotherEntity('two');
}
$query = $this->entityStorage
->getQuery()
->sort($this->entity
->getEntityType()
->getKey('id'));
return $this->entityStorage
->loadMultiple($query
->execute());
}
protected function normalize(EntityInterface $entity, Url $url) {
return $this->serializer
->normalize(new JsonApiDocumentTopLevel($entity), 'api_json', [
'resource_type' => $this->container
->get('jsonapi.resource_type.repository')
->getByTypeName(static::$resourceTypeName),
'request' => Request::create($url
->toString(TRUE)
->getGeneratedUrl()),
])
->rasterizeValue();
}
protected abstract function createEntity();
protected function createAnotherEntity($key) {
$duplicate = $this
->getEntityDuplicate($this->entity, $key);
if (get_class($this->entityStorage) !== ContentEntityNullStorage::class) {
$duplicate
->set('field_rest_test', 'Second collection entity');
}
$duplicate
->save();
return $duplicate;
}
protected function getEntityDuplicate(EntityInterface $original, $key) {
$duplicate = $original
->createDuplicate();
if ($label_key = $original
->getEntityType()
->getKey('label')) {
$duplicate
->set($label_key, $original
->label() . '_' . $key);
}
if ($duplicate instanceof ConfigEntityInterface && ($id_key = $duplicate
->getEntityType()
->getKey('id'))) {
$id = $original
->id();
$id_key = $duplicate
->getEntityType()
->getKey('id');
$duplicate
->set($id_key, $id . '_' . $key);
}
return $duplicate;
}
protected abstract function getExpectedDocument();
protected abstract function getPostDocument();
protected function getPatchDocument() {
return NestedArray::mergeDeep([
'data' => [
'id' => $this->entity
->uuid(),
],
], $this
->getPostDocument());
}
protected function getExpectedUnauthorizedAccessCacheability() {
return (new CacheableMetadata())
->setCacheTags([
'4xx-response',
'http_response',
])
->setCacheContexts([
'user.permissions',
]);
}
protected function getExpectedCacheTags(array $sparse_fieldset = NULL) {
$expected_cache_tags = [
'http_response',
];
return Cache::mergeTags($expected_cache_tags, $this->entity
->getCacheTags());
}
protected function getExpectedCacheContexts(array $sparse_fieldset = NULL) {
return [
'url.query_args:fields',
'url.query_args:filter',
'url.query_args:include',
'url.query_args:page',
'url.query_args:sort',
'url.site',
'user.permissions',
];
}
protected static function getExpectedCollectionCacheability(array $collection, array $sparse_fieldset = NULL, AccountInterface $account, $filtered = FALSE) {
$cacheability = array_reduce($collection, function (CacheableMetadata $cacheability, EntityInterface $entity) use ($sparse_fieldset, $account) {
$access_result = static::entityAccess($entity, 'view', $account);
$cacheability
->addCacheableDependency($access_result);
if ($access_result
->isAllowed()) {
$cacheability
->addCacheableDependency($entity);
if ($entity instanceof FieldableEntityInterface) {
foreach ($entity as $field_name => $field_item_list) {
if (is_null($sparse_fieldset) || in_array($field_name, $sparse_fieldset)) {
$field_access = static::entityFieldAccess($entity, $field_name, 'view', $account);
$cacheability
->addCacheableDependency($field_access);
if ($field_access
->isAllowed()) {
foreach ($field_item_list as $field_item) {
foreach (TypedDataInternalPropertiesHelper::getNonInternalProperties($field_item) as $property) {
$cacheability
->addCacheableDependency(CacheableMetadata::createFromObject($property));
}
}
}
}
}
}
}
return $cacheability;
}, new CacheableMetadata());
$cacheability
->addCacheTags([
'http_response',
]);
$cacheability
->addCacheTags(reset($collection)
->getEntityType()
->getListCacheTags());
$cacheability
->addCacheContexts([
'url.query_args:fields',
'url.query_args:filter',
'url.query_args:include',
'url.query_args:page',
'url.query_args:sort',
'url.site',
]);
return $cacheability;
}
protected abstract function setUpAuthorization($method);
protected function getExpectedUnauthorizedAccessMessage($method) {
$permission = $this->entity
->getEntityType()
->getAdminPermission();
if ($permission !== FALSE) {
return "The '{$permission}' permission is required.";
}
return NULL;
}
protected function grantPermissionsToTestedRole(array $permissions) {
$this
->grantPermissions(Role::load(RoleInterface::AUTHENTICATED_ID), $permissions);
}
protected function revokePermissionsFromTestedRole(array $permissions) {
$role = Role::load(RoleInterface::AUTHENTICATED_ID);
foreach ($permissions as $permission) {
$role
->revokePermission($permission);
}
$role
->trustData()
->save();
}
protected function request($method, Url $url, array $request_options) {
$this
->refreshVariables();
$request_options[RequestOptions::HTTP_ERRORS] = FALSE;
$request_options[RequestOptions::ALLOW_REDIRECTS] = FALSE;
$request_options = $this
->decorateWithXdebugCookie($request_options);
$client = $this
->getSession()
->getDriver()
->getClient()
->getClient();
return $client
->request($method, $url
->setAbsolute(TRUE)
->toString(), $request_options);
}
protected function assertResourceResponse($expected_status_code, $expected_document, ResponseInterface $response, $expected_cache_tags = FALSE, $expected_cache_contexts = FALSE, $expected_page_cache_header_value = FALSE, $expected_dynamic_page_cache_header_value = FALSE) {
$this
->assertSame($expected_status_code, $response
->getStatusCode());
if ($expected_status_code === 204) {
$this
->assertSame('', (string) $response
->getBody());
}
else {
$this
->assertSame([
'application/vnd.api+json',
], $response
->getHeader('Content-Type'));
if ($expected_document !== FALSE) {
$response_document = Json::decode((string) $response
->getBody());
if ($expected_document === NULL) {
$this
->assertNull($response_document);
}
else {
$this
->assertSameDocument($expected_document, $response_document);
}
}
}
$this
->assertSame($expected_cache_tags !== FALSE, $response
->hasHeader('X-Drupal-Cache-Tags'));
if (is_array($expected_cache_tags)) {
$this
->assertSame($expected_cache_tags, explode(' ', $response
->getHeader('X-Drupal-Cache-Tags')[0]));
}
$this
->assertSame($expected_cache_contexts !== FALSE, $response
->hasHeader('X-Drupal-Cache-Contexts'));
if (is_array($expected_cache_contexts)) {
$optimized_expected_cache_contexts = \Drupal::service('cache_contexts_manager')
->optimizeTokens($expected_cache_contexts);
$this
->assertSame($optimized_expected_cache_contexts, explode(' ', $response
->getHeader('X-Drupal-Cache-Contexts')[0]));
}
if ($expected_page_cache_header_value !== FALSE) {
$this
->assertTrue($response
->hasHeader('X-Drupal-Cache'));
$this
->assertSame($expected_page_cache_header_value, $response
->getHeader('X-Drupal-Cache')[0]);
}
else {
$this
->assertFalse($response
->hasHeader('X-Drupal-Cache'));
}
if ($expected_dynamic_page_cache_header_value !== FALSE) {
$this
->assertTrue($response
->hasHeader('X-Drupal-Dynamic-Cache'));
$this
->assertSame($expected_dynamic_page_cache_header_value, $response
->getHeader('X-Drupal-Dynamic-Cache')[0]);
}
else {
$this
->assertFalse($response
->hasHeader('X-Drupal-Dynamic-Cache'));
}
}
protected function assertSameDocument(array $expected_document, array $actual_document) {
static::recursiveKsort($expected_document);
static::recursiveKsort($actual_document);
if (!empty($expected_document['included'])) {
static::sortResourceCollection($expected_document['included']);
static::sortResourceCollection($actual_document['included']);
}
if (isset($actual_document['errors']) && isset($expected_document['errors'])) {
$actual_errors =& $actual_document['errors'];
static::sortErrors($actual_errors);
$expected_errors =& $expected_document['errors'];
static::sortErrors($expected_errors);
}
if (isset($actual_document['meta']['errors']) && isset($expected_document['meta']['errors'])) {
$actual_errors =& $actual_document['meta']['errors'];
static::sortErrors($actual_errors);
$expected_errors =& $expected_document['meta']['errors'];
static::sortErrors($expected_errors);
}
$strip_error_identifiers = function (&$document) {
if (isset($document['errors'])) {
foreach ($document['errors'] as &$error) {
unset($error['id']);
}
}
if (isset($document['meta']['errors'])) {
foreach ($document['meta']['errors'] as &$error) {
unset($error['id']);
}
}
};
$strip_error_identifiers($expected_document);
$strip_error_identifiers($actual_document);
$expected_keys = array_keys($expected_document);
$actual_keys = array_keys($actual_document);
$missing_member_names = array_diff($expected_keys, $actual_keys);
$extra_member_names = array_diff($actual_keys, $expected_keys);
if (!empty($missing_member_names) || !empty($extra_member_names)) {
$message_format = "The document members did not match the expected values. Missing: [ %s ]. Unexpected: [ %s ]";
$message = sprintf($message_format, implode(', ', $missing_member_names), implode(', ', $extra_member_names));
$this
->assertSame($expected_document, $actual_document, $message);
}
foreach ($expected_document as $member_name => $expected_member) {
$actual_member = $actual_document[$member_name];
$this
->assertSame($expected_member, $actual_member, "The '{$member_name}' member was not as expected.");
}
}
protected function assertResourceErrorResponse($expected_status_code, $expected_message, ResponseInterface $response, $pointer = FALSE, $expected_cache_tags = FALSE, $expected_cache_contexts = FALSE, $expected_page_cache_header_value = FALSE, $expected_dynamic_page_cache_header_value = FALSE) {
$expected_error = [];
if (!empty(Response::$statusTexts[$expected_status_code])) {
$expected_error['title'] = Response::$statusTexts[$expected_status_code];
}
$expected_error['status'] = $expected_status_code;
$expected_error['detail'] = $expected_message;
if ($info_url = HttpExceptionNormalizer::getInfoUrl($expected_status_code)) {
$expected_error['links']['info'] = $info_url;
}
$expected_error['code'] = 0;
if ($pointer !== FALSE) {
$expected_error['source']['pointer'] = $pointer;
}
$expected_document = [
'errors' => [
0 => $expected_error,
],
];
$this
->assertResourceResponse($expected_status_code, $expected_document, $response, $expected_cache_tags, $expected_cache_contexts, $expected_page_cache_header_value, $expected_dynamic_page_cache_header_value);
}
protected function decorateWithXdebugCookie(array $request_options) {
$session = $this
->getSession();
$driver = $session
->getDriver();
if ($driver instanceof BrowserKitDriver) {
$client = $driver
->getClient();
foreach ($client
->getCookieJar()
->all() as $cookie) {
if (isset($request_options[RequestOptions::HEADERS]['Cookie'])) {
$request_options[RequestOptions::HEADERS]['Cookie'] .= '; ' . $cookie
->getName() . '=' . $cookie
->getValue();
}
else {
$request_options[RequestOptions::HEADERS]['Cookie'] = $cookie
->getName() . '=' . $cookie
->getValue();
}
}
}
return $request_options;
}
protected function removeResourceTypeFromDocument(array $document) {
unset($document['data']['type']);
return $document;
}
protected function makeNormalizationInvalid(array $document, $entity_key) {
$entity_type = $this->entity
->getEntityType();
switch ($entity_key) {
case 'label':
$label_field = $entity_type
->hasKey('label') ? $entity_type
->getKey('label') : static::$labelFieldName;
$document['data']['attributes'][$label_field] = [
0 => $document['data']['attributes'][$label_field],
1 => 'Second Title',
];
break;
case 'id':
$document['data']['attributes'][$entity_type
->getKey('id')] = $this->anotherEntity
->id();
break;
case 'uuid':
$document['data']['id'] = $this->anotherEntity
->uuid();
break;
}
return $document;
}
public function testGetIndividual() {
$url = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), [
static::$entityTypeId => $this->entity
->uuid(),
]);
$request_options = [];
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
$request_options = NestedArray::mergeDeep($request_options, $this
->getAuthenticationRequestOptions());
$response = $this
->request('GET', $url, $request_options);
$reason = $this
->getExpectedUnauthorizedAccessMessage('GET');
$message = trim("The current user is not allowed to GET the selected resource. {$reason}");
$this
->assertResourceErrorResponse(403, $message, $response, '/data');
$this
->assertArrayNotHasKey('Link', $response
->getHeaders());
$this
->setUpAuthorization('GET');
$request_options[RequestOptions::BODY] = Json::encode($this
->getExpectedDocument());
$response = $this
->request('HEAD', $url, $request_options);
$this
->assertResourceResponse(200, NULL, $response, $this
->getExpectedCacheTags(), $this
->getExpectedCacheContexts(), FALSE, 'MISS');
$head_headers = $response
->getHeaders();
$response = $this
->request('GET', $url, $request_options);
$this
->assertResourceResponse(200, $this
->getExpectedDocument(), $response, $this
->getExpectedCacheTags(), $this
->getExpectedCacheContexts(), FALSE, 'HIT');
$cache_items = $this->container
->get('database')
->query("SELECT cid, data FROM {cache_dynamic_page_cache} WHERE cid LIKE :pattern", [
':pattern' => '%[route]=jsonapi.%',
])
->fetchAllAssoc('cid');
$this
->assertTrue(count($cache_items) >= 2);
$found_cache_redirect = FALSE;
$found_cached_200_response = FALSE;
$other_cached_responses_are_4xx = TRUE;
foreach ($cache_items as $cid => $cache_item) {
$cached_data = unserialize($cache_item->data);
if (!isset($cached_data['#cache_redirect'])) {
$cached_response = $cached_data['#response'];
if ($cached_response
->getStatusCode() === 200) {
$found_cached_200_response = TRUE;
}
elseif (!$cached_response
->isClientError()) {
$other_cached_responses_are_4xx = FALSE;
}
$this
->assertNotInstanceOf(ResourceResponse::class, $cached_response);
$this
->assertInstanceOf(CacheableResponseInterface::class, $cached_response);
}
else {
$found_cache_redirect = TRUE;
}
}
$this
->assertTrue($found_cache_redirect);
$this
->assertTrue($found_cached_200_response);
$this
->assertTrue($other_cached_responses_are_4xx);
$unserialized = $this->serializer
->deserialize((string) $response
->getBody(), JsonApiDocumentTopLevel::class, 'api_json', [
'target_entity' => static::$entityTypeId,
'resource_type' => $this->container
->get('jsonapi.resource_type.repository')
->getByTypeName(static::$resourceTypeName),
]);
$this
->assertSame($unserialized
->uuid(), $this->entity
->uuid());
$get_headers = $response
->getHeaders();
$ignored_headers = [
'Date',
'Content-Length',
'X-Drupal-Cache',
'X-Drupal-Dynamic-Cache',
'Transfer-Encoding',
'Vary',
];
$header_cleaner = function ($headers) use ($ignored_headers) {
foreach ($headers as $header => $value) {
if (strpos($header, 'X-Drupal-Assertion-') === 0 || in_array($header, $ignored_headers)) {
unset($headers[$header]);
}
}
return $headers;
};
$get_headers = $header_cleaner($get_headers);
$head_headers = $header_cleaner($head_headers);
$this
->assertSame($get_headers, $head_headers);
$this
->doTestSparseFieldSets($url, $request_options);
$this
->doTestIncluded($url, $request_options);
$random_uuid = \Drupal::service('uuid')
->generate();
$url = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), [
static::$entityTypeId => $random_uuid,
]);
$response = $this
->request('GET', $url, $request_options);
$message_url = clone $url;
$path = str_replace($random_uuid, '{' . static::$entityTypeId . '}', $message_url
->setAbsolute()
->setOptions([
'base_url' => '',
'query' => [],
])
->toString());
$message = 'The "' . static::$entityTypeId . '" parameter was not converted for the path "' . $path . '" (route name: "jsonapi.' . static::$resourceTypeName . '.individual")';
$this
->assertResourceErrorResponse(404, $message, $response);
unset($request_options[RequestOptions::HEADERS]['Accept']);
$response = $this
->request('GET', $url, $request_options);
$this
->assertSame(404, $response
->getStatusCode());
$this
->assertSame([
'text/html; charset=UTF-8',
], $response
->getHeader('Content-Type'));
}
public function testCollection() {
$entity_collection = $this
->getEntityCollection();
assert(count($entity_collection) > 1, 'A collection must have more that one entity in it.');
$collection_url = Url::fromRoute(sprintf('jsonapi.%s.collection', static::$resourceTypeName))
->setAbsolute(TRUE);
$request_options = [];
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
$request_options = NestedArray::mergeDeep($request_options, $this
->getAuthenticationRequestOptions());
$expected_response = $this
->getExpectedCollectionResponse($entity_collection, $collection_url
->toString(), $request_options);
$expected_cacheability = $expected_response
->getCacheableMetadata();
$expected_document = $expected_response
->getResponseData();
$response = $this
->request('GET', $collection_url, $request_options);
$dynamic_cache = $expected_cacheability
->getCacheMaxAge() === 0 ? 'UNCACHEABLE' : 'MISS';
$this
->assertResourceResponse(200, $expected_document, $response, $expected_cacheability
->getCacheTags(), $expected_cacheability
->getCacheContexts(), FALSE, $dynamic_cache);
$this
->setUpAuthorization('GET');
$expected_response = $this
->getExpectedCollectionResponse($entity_collection, $collection_url
->toString(), $request_options);
$expected_cacheability = $expected_response
->getCacheableMetadata();
$response = $this
->request('HEAD', $collection_url, $request_options);
$this
->assertResourceResponse(200, NULL, $response, $expected_cacheability
->getCacheTags(), $expected_cacheability
->getCacheContexts(), FALSE, $dynamic_cache);
$expected_response = $this
->getExpectedCollectionResponse($entity_collection, $collection_url
->toString(), $request_options);
$expected_cacheability = $expected_response
->getCacheableMetadata();
$expected_document = $expected_response
->getResponseData();
$response = $this
->request('GET', $collection_url, $request_options);
$dynamic_cache = $dynamic_cache === 'UNCACHEABLE' ? 'UNCACHEABLE' : 'HIT';
$this
->assertResourceResponse(200, $expected_document, $response, $expected_cacheability
->getCacheTags(), $expected_cacheability
->getCacheContexts(), FALSE, $dynamic_cache);
if ($this->entity instanceof FieldableEntityInterface) {
$unauthorized_filter_url = clone $collection_url;
$unauthorized_filter_url
->setOption('query', [
'filter' => [
'related_author_id' => [
'operator' => '<>',
'path' => 'field_jsonapi_test_entity_ref.mail',
'value' => 'doesnt@matter.com',
],
],
]);
$response = $this
->request('GET', $unauthorized_filter_url, $request_options);
$expected_error_message = "The current user is not authorized to filter by the `field_jsonapi_test_entity_ref` field, given in the path `field_jsonapi_test_entity_ref`. The 'field_jsonapi_test_entity_ref view access' permission is required.";
$this
->assertResourceErrorResponse(403, $expected_error_message, $response);
$this
->grantPermissionsToTestedRole([
'field_jsonapi_test_entity_ref view access',
]);
$response = $this
->request('GET', $unauthorized_filter_url, $request_options);
$expected_error_message = "The current user is not authorized to filter by the `mail` field, given in the path `field_jsonapi_test_entity_ref.entity:user.mail`.";
$this
->assertResourceErrorResponse(403, $expected_error_message, $response);
}
$filtered_entity_collection = $entity_collection;
$removed = array_shift($filtered_entity_collection);
$filtered_collection_url = clone $collection_url;
$entity_collection_filter = [
'filter' => [
'ids' => [
'condition' => [
'operator' => '<>',
'path' => $removed
->getEntityType()
->getKey('id'),
'value' => $removed
->id(),
],
],
],
];
$filtered_collection_url
->setOption('query', $entity_collection_filter);
$expected_response = $this
->getExpectedCollectionResponse($filtered_entity_collection, $filtered_collection_url
->toString(), $request_options, TRUE);
$expected_cacheability = $expected_response
->getCacheableMetadata();
$expected_document = $expected_response
->getResponseData();
$response = $this
->request('GET', $filtered_collection_url, $request_options);
$dynamic_cache = $expected_cacheability
->getCacheMaxAge() === 0 || !empty(array_intersect([
'user',
'session',
], $expected_cacheability
->getCacheContexts())) ? 'UNCACHEABLE' : 'MISS';
$this
->assertResourceResponse(200, $expected_document, $response, $expected_cacheability
->getCacheTags(), $expected_cacheability
->getCacheContexts(), FALSE, $dynamic_cache);
$relationship_field_names = array_reduce($filtered_entity_collection, function ($relationship_field_names, $entity) {
return array_unique(array_merge($relationship_field_names, $this
->getRelationshipFieldNames($entity)));
}, []);
$include = [
'include' => implode(',', $relationship_field_names),
];
$filtered_collection_include_url = clone $collection_url;
$filtered_collection_include_url
->setOption('query', array_merge($entity_collection_filter, $include));
$expected_response = $this
->getExpectedCollectionResponse($filtered_entity_collection, $filtered_collection_include_url
->toString(), $request_options, TRUE);
$related_responses = array_reduce($filtered_entity_collection, function ($related_responses, $entity) use ($relationship_field_names, $request_options) {
return array_merge($related_responses, array_values($this
->getExpectedRelatedResponses($relationship_field_names, $request_options, $entity)));
}, []);
$expected_response = static::decorateExpectedResponseForIncludedFields($expected_response, $related_responses);
$expected_cacheability = $expected_response
->getCacheableMetadata();
$expected_document = $expected_response
->getResponseData();
if (!empty($expected_document['meta']['errors'])) {
foreach ($expected_document['meta']['errors'] as $index => $error) {
$expected_document['meta']['errors'][$index]['source']['pointer'] = '/data';
}
}
$response = $this
->request('GET', $filtered_collection_include_url, $request_options);
$dynamic_cache = $expected_cacheability
->getCacheMaxAge() === 0 || !empty(array_intersect([
'user',
'session',
], $expected_cacheability
->getCacheContexts())) ? 'UNCACHEABLE' : 'MISS';
$this
->assertResourceResponse(200, $expected_document, $response, $expected_cacheability
->getCacheTags(), $expected_cacheability
->getCacheContexts(), FALSE, $dynamic_cache);
$sorted_entity_collection = $entity_collection;
uasort($sorted_entity_collection, function (EntityInterface $a, EntityInterface $b) {
return strcmp($b
->id(), $a
->id());
});
$id_key = reset($entity_collection)
->getEntityType()
->getKey('id');
if (!$id_key) {
return;
}
$sorted_collection_include_url = clone $collection_url;
$sorted_collection_include_url
->setOption('query', array_merge($include, [
'sort' => "-{$id_key}",
]));
$expected_response = $this
->getExpectedCollectionResponse($sorted_entity_collection, $sorted_collection_include_url
->toString(), $request_options);
$related_responses = array_reduce($sorted_entity_collection, function ($related_responses, $entity) use ($relationship_field_names, $request_options) {
return array_merge($related_responses, array_values($this
->getExpectedRelatedResponses($relationship_field_names, $request_options, $entity)));
}, []);
$expected_response = static::decorateExpectedResponseForIncludedFields($expected_response, $related_responses);
$expected_cacheability = $expected_response
->getCacheableMetadata();
$expected_document = $expected_response
->getResponseData();
if (!empty($expected_document['meta']['errors'])) {
foreach ($expected_document['meta']['errors'] as $index => $error) {
$expected_document['meta']['errors'][$index]['source']['pointer'] = '/data';
}
}
$response = $this
->request('GET', $sorted_collection_include_url, $request_options);
$dynamic_cache = $expected_cacheability
->getCacheMaxAge() === 0 ? 'UNCACHEABLE' : 'MISS';
$this
->assertResourceResponse(200, $expected_document, $response, $expected_cacheability
->getCacheTags(), $expected_cacheability
->getCacheContexts(), FALSE, $dynamic_cache);
}
protected function getExpectedCollectionResponse(array $collection, $self_link, array $request_options, $filtered = FALSE) {
$resource_identifiers = array_map([
static::class,
'toResourceIdentifier',
], $collection);
$individual_responses = static::toResourceResponses($this
->getResponses(static::getResourceLinks($resource_identifiers), $request_options));
$merged_response = static::toCollectionResourceResponse($individual_responses, $self_link, TRUE);
$merged_document = $merged_response
->getResponseData();
if (!isset($merged_document['data'])) {
$merged_document['data'] = [];
}
if (!empty($merged_document['meta']['errors'])) {
foreach ($merged_document['meta']['errors'] as $index => $error) {
$merged_document['meta']['errors'][$index]['source']['pointer'] = '/data';
}
}
$cacheability = static::getExpectedCollectionCacheability($collection, NULL, $this->account, $filtered);
$cacheability
->setCacheMaxAge($merged_response
->getCacheableMetadata()
->getCacheMaxAge());
$collection_response = ResourceResponse::create($merged_document);
$collection_response
->addCacheableDependency($cacheability);
return $collection_response;
}
public function testRelated() {
$request_options = [];
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
$request_options = NestedArray::mergeDeep($request_options, $this
->getAuthenticationRequestOptions());
$this
->doTestRelated($request_options);
$this
->setUpAuthorization('GET');
$this
->doTestRelated($request_options);
}
public function testRelationships() {
if ($this->entity instanceof ConfigEntityInterface) {
$this
->markTestSkipped('Configuration entities cannot have relationships.');
}
$request_options = [];
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
$request_options = NestedArray::mergeDeep($request_options, $this
->getAuthenticationRequestOptions());
$this
->doTestRelationshipGet($request_options);
$this
->setUpAuthorization('GET');
$this
->doTestRelationshipGet($request_options);
$this
->doTestRelationshipPost($request_options);
$this
->setUpAuthorization('PATCH');
$this
->doTestRelationshipPost($request_options);
$this
->grantPermissionsToTestedRole([
'field_jsonapi_test_entity_ref view access',
'field_jsonapi_test_entity_ref edit access',
'field_jsonapi_test_entity_ref update access',
]);
$this
->doTestRelationshipPost($request_options);
}
protected function doTestRelated(array $request_options) {
$relationship_field_names = $this
->getRelationshipFieldNames($this->entity);
if (empty($relationship_field_names)) {
return;
}
$expected_relationship_responses = $this
->getExpectedRelatedResponses($relationship_field_names, $request_options);
$relationship_responses = $this
->getRelatedResponses($relationship_field_names, $request_options);
foreach ($relationship_field_names as $relationship_field_name) {
$expected_resource_response = $expected_relationship_responses[$relationship_field_name];
$actual_response = $relationship_responses[$relationship_field_name];
$this
->assertSame($expected_resource_response
->getStatusCode(), $actual_response
->getStatusCode());
$expected_document = $expected_resource_response
->getResponseData();
$actual_document = Json::decode((string) $actual_response
->getBody());
$this
->assertSameDocument($expected_document, $actual_document);
}
}
protected function doTestRelationshipGet(array $request_options) {
$relationship_field_names = $this
->getRelationshipFieldNames($this->entity);
if (empty($relationship_field_names)) {
return;
}
$related_responses = $this
->getRelationshipResponses($relationship_field_names, $request_options);
foreach ($relationship_field_names as $relationship_field_name) {
$expected_resource_response = $this
->getExpectedGetRelationshipResponse($relationship_field_name);
$expected_document = $expected_resource_response
->getResponseData();
$actual_response = $related_responses[$relationship_field_name];
$actual_document = Json::decode((string) $actual_response
->getBody());
$this
->assertSameDocument($expected_document, $actual_document);
$this
->assertSame($expected_resource_response
->getStatusCode(), $actual_response
->getStatusCode());
}
}
protected function doTestRelationshipPost(array $request_options) {
$resource = $this
->createAnotherEntity('dupe');
$resource
->set('field_jsonapi_test_entity_ref', NULL);
$violations = $resource
->validate();
assert($violations
->count() === 0, (string) $violations);
$resource
->save();
$target_resource = $this
->createUser();
$violations = $target_resource
->validate();
assert($violations
->count() === 0, (string) $violations);
$target_resource
->save();
$target_identifier = static::toResourceIdentifier($target_resource);
$resource_identifier = static::toResourceIdentifier($resource);
$relationship_field_name = 'field_jsonapi_test_entity_ref';
$update_access = static::entityAccess($resource, 'update', $this->account)
->andIf(static::entityFieldAccess($resource, $relationship_field_name, 'update', $this->account));
$url = Url::fromRoute(sprintf("jsonapi.{$resource_identifier['type']}.relationship"), [
'related' => $relationship_field_name,
$resource
->getEntityTypeId() => $resource
->uuid(),
]);
if ($update_access
->isAllowed()) {
$response = $this
->request('POST', $url, $request_options);
$this
->assertResourceErrorResponse(400, 'Empty request body.', $response);
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceErrorResponse(400, 'Empty request body.', $response);
$request_options[RequestOptions::BODY] = Json::encode([
'data' => [],
]);
$response = $this
->request('POST', $url, $request_options);
$this
->assertResourceResponse(204, NULL, $response);
$request_options[RequestOptions::BODY] = Json::encode([
'data' => [],
]);
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceResponse(204, NULL, $response);
$request_options[RequestOptions::BODY] = Json::encode([
'data' => $target_identifier,
]);
$response = $this
->request('POST', $url, $request_options);
$this
->assertResourceErrorResponse(400, 'Invalid body payload for the relationship.', $response);
$request_options[RequestOptions::BODY] = Json::encode([
'data' => $target_identifier,
]);
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceErrorResponse(400, 'Invalid body payload for the relationship.', $response);
$request_options[RequestOptions::BODY] = Json::encode([
'data' => array_intersect_key($target_identifier, [
'id' => 'id',
]),
]);
$response = $this
->request('POST', $url, $request_options);
$this
->assertResourceErrorResponse(400, 'Invalid body payload for the relationship.', $response);
$request_options[RequestOptions::BODY] = Json::encode([
'data' => array_intersect_key($target_identifier, [
'id' => 'id',
]),
]);
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceErrorResponse(400, 'Invalid body payload for the relationship.', $response);
if (static::$resourceTypeName !== $target_identifier['type']) {
$request_options[RequestOptions::BODY] = Json::encode([
'data' => [
$resource_identifier,
],
]);
$response = $this
->request('POST', $url, $request_options);
$this
->assertResourceErrorResponse(400, sprintf('The provided type (%s) does not mach the destination resource types (%s).', $resource_identifier['type'], $target_identifier['type']), $response);
$request_options[RequestOptions::BODY] = Json::encode([
'data' => [
$resource_identifier,
],
]);
$response = $this
->request('POST', $url, $request_options);
$this
->assertResourceErrorResponse(400, sprintf('The provided type (%s) does not mach the destination resource types (%s).', $resource_identifier['type'], $target_identifier['type']), $response);
}
$request_options[RequestOptions::BODY] = Json::encode([
'data' => [
$target_identifier,
],
]);
$response = $this
->request('POST', $url, $request_options);
$resource
->set($relationship_field_name, [
$target_resource,
]);
$this
->assertResourceResponse(204, NULL, $response);
$request_options[RequestOptions::BODY] = Json::encode([
'data' => [
$target_identifier,
],
]);
$response = $this
->request('PATCH', $url, $request_options);
$resource
->set($relationship_field_name, [
$target_resource,
]);
$this
->assertResourceResponse(204, NULL, $response);
$request_options[RequestOptions::BODY] = Json::encode([
'data' => [
$target_identifier + [
'meta' => [
'arity' => 1,
],
],
],
]);
$response = $this
->request('POST', $url, $request_options);
$resource
->set($relationship_field_name, [
$target_resource,
$target_resource,
]);
$expected_document = $this
->getExpectedGetRelationshipDocument($relationship_field_name, $resource);
$expected_document['data'][0] += [
'meta' => [
'arity' => 0,
],
];
$expected_document['data'][1] += [
'meta' => [
'arity' => 1,
],
];
$this
->assertResourceResponse(200, $expected_document, $response);
$request_options[RequestOptions::BODY] = Json::encode([
'data' => [
$target_identifier,
],
]);
$response = $this
->request('DELETE', $url, $request_options);
$resource
->set($relationship_field_name, []);
$this
->assertResourceResponse(204, NULL, $response);
$expected_document = $this
->getExpectedGetRelationshipDocument($relationship_field_name, $resource);
$response = $this
->request('GET', $url, $request_options);
$this
->assertSameDocument($expected_document, Json::decode((string) $response
->getBody()));
$request_options[RequestOptions::BODY] = Json::encode([
'data' => [
$target_identifier,
],
]);
$response = $this
->request('DELETE', $url, $request_options);
$this
->assertResourceResponse(204, NULL, $response);
$expected_document = $this
->getExpectedGetRelationshipDocument($relationship_field_name, $resource);
$response = $this
->request('GET', $url, $request_options);
$this
->assertSameDocument($expected_document, Json::decode((string) $response
->getBody()));
$request_options[RequestOptions::BODY] = Json::encode([
'data' => [
$target_identifier,
$target_identifier,
],
]);
$response = $this
->request('PATCH', $url, $request_options);
$resource
->set($relationship_field_name, [
$target_resource,
$target_resource,
]);
$expected_document = $this
->getExpectedGetRelationshipDocument($relationship_field_name, $resource);
$expected_document['data'][0] += [
'meta' => [
'arity' => 0,
],
];
$expected_document['data'][1] += [
'meta' => [
'arity' => 1,
],
];
$this
->assertResourceResponse(204, NULL, $response);
$request_options[RequestOptions::BODY] = Json::encode([
'data' => [
$target_identifier,
],
]);
$response = $this
->request('DELETE', $url, $request_options);
$resource
->set($relationship_field_name, []);
$this
->assertResourceResponse(204, NULL, $response);
$resource
->set($relationship_field_name, []);
$expected_document = $this
->getExpectedGetRelationshipDocument($relationship_field_name, $resource);
$response = $this
->request('GET', $url, $request_options);
$this
->assertSameDocument($expected_document, Json::decode((string) $response
->getBody()));
}
else {
$request_options[RequestOptions::BODY] = Json::encode([
'data' => [
$target_identifier,
],
]);
$response = $this
->request('POST', $url, $request_options);
$message = 'The current user is not allowed to update this relationship.';
$message .= ($reason = $update_access
->getReason()) ? ' ' . $reason : '';
$this
->assertResourceErrorResponse(403, $message, $response, $relationship_field_name);
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceErrorResponse(403, $message, $response, $relationship_field_name);
$response = $this
->request('DELETE', $url, $request_options);
$this
->assertResourceErrorResponse(403, $message, $response, $relationship_field_name);
}
$resource
->delete();
$target_resource
->delete();
}
protected function getExpectedGetRelationshipResponse($relationship_field_name, EntityInterface $entity = NULL) {
$entity = $entity ?: $this->entity;
$access = static::entityFieldAccess($entity, $relationship_field_name, 'view', $this->account);
if (!$access
->isAllowed()) {
return static::getAccessDeniedResponse($this->entity, $access, $relationship_field_name, 'The current user is not allowed to view this relationship.');
}
$expected_document = $this
->getExpectedGetRelationshipDocument($relationship_field_name);
$status_code = isset($expected_document['errors'][0]['status']) ? $expected_document['errors'][0]['status'] : 200;
$resource_response = new ResourceResponse($expected_document, $status_code);
return $resource_response;
}
protected function getExpectedGetRelationshipDocument($relationship_field_name, EntityInterface $entity = NULL) {
$entity = $entity ?: $this->entity;
$entity_type_id = $entity
->getEntityTypeId();
$bundle = $entity
->bundle();
$id = $entity
->uuid();
$self_link = Url::fromUri("base:/jsonapi/{$entity_type_id}/{$bundle}/{$id}/relationships/{$relationship_field_name}")
->setAbsolute()
->toString(TRUE)
->getGeneratedUrl();
$related_link = Url::fromUri("base:/jsonapi/{$entity_type_id}/{$bundle}/{$id}/{$relationship_field_name}")
->setAbsolute()
->toString(TRUE)
->getGeneratedUrl();
$data = $this
->getExpectedGetRelationshipDocumentData($relationship_field_name, $entity);
return [
'data' => $data,
'jsonapi' => [
'meta' => [
'links' => [
'self' => 'http://jsonapi.org/format/1.0/',
],
],
'version' => '1.0',
],
'links' => [
'self' => $self_link,
'related' => $related_link,
],
];
}
protected function getExpectedGetRelationshipDocumentData($relationship_field_name, EntityInterface $entity = NULL) {
$entity = $entity ?: $this->entity;
$field = $entity->{$relationship_field_name};
$is_multiple = $field
->getFieldDefinition()
->getFieldStorageDefinition()
->getCardinality() !== 1;
if ($field
->isEmpty()) {
return $is_multiple ? [] : NULL;
}
if (!$is_multiple) {
$target_entity = $field->entity;
return is_null($target_entity) ? NULL : static::toResourceIdentifier($target_entity);
}
else {
return array_filter(array_map(function ($item) {
$target_entity = $item->entity;
return is_null($target_entity) ? NULL : static::toResourceIdentifier($target_entity);
}, iterator_to_array($field)));
}
}
protected function getExpectedRelatedResponses(array $relationship_field_names, array $request_options, EntityInterface $entity = NULL) {
$entity = $entity ?: $this->entity;
$relationship_responses = array_map(function ($relationship_field_name) use ($entity) {
return $this
->getExpectedGetRelationshipResponse($relationship_field_name, $entity);
}, array_combine($relationship_field_names, $relationship_field_names));
$base_resource_identifier = static::toResourceIdentifier($entity);
$expected_related_responses = [];
foreach ($relationship_field_names as $relationship_field_name) {
$access = static::entityFieldAccess($entity, $relationship_field_name, 'view', $this->account);
if (!$access
->isAllowed()) {
$detail = 'The current user is not allowed to view this relationship.';
if ($access instanceof AccessResultReasonInterface && ($reason = $access
->getReason())) {
$detail .= ' ' . $reason;
}
$related_response = (new ResourceResponse([
'errors' => [
[
'status' => 403,
'title' => 'Forbidden',
'detail' => $detail,
'links' => [
'info' => HttpExceptionNormalizer::getInfoUrl(403),
],
'code' => 0,
'id' => '/' . $base_resource_identifier['type'] . '/' . $base_resource_identifier['id'],
'source' => [
'pointer' => $relationship_field_name,
],
],
],
], 403))
->addCacheableDependency($access);
}
else {
$self_link = static::getRelatedLink($base_resource_identifier, $relationship_field_name);
$relationship_response = $relationship_responses[$relationship_field_name];
$relationship_document = $relationship_response
->getResponseData();
if (empty($relationship_document['data'])) {
$related_response = isset($relationship_document['errors']) ? $relationship_response : new ResourceResponse([
'data' => is_null($relationship_document['data']) ? NULL : [],
'jsonapi' => [
'meta' => [
'links' => [
'self' => 'http://jsonapi.org/format/1.0/',
],
],
'version' => '1.0',
],
'links' => [
'self' => $self_link,
],
]);
}
else {
$is_to_one_relationship = static::isResourceIdentifier($relationship_document['data']);
$resource_identifiers = $is_to_one_relationship ? [
$relationship_document['data'],
] : $relationship_document['data'];
$individual_responses = static::toResourceResponses($this
->getResponses(static::getResourceLinks($resource_identifiers), $request_options));
$related_response = static::toCollectionResourceResponse($individual_responses, $self_link, !$is_to_one_relationship);
}
}
$expected_related_responses[$relationship_field_name] = $related_response;
}
return $expected_related_responses ?: [];
}
protected function getExpectedIncludeResponse(array $include_paths, array $request_options) {
$individual_response = $this
->getExpectedGetIndividualResourceResponse();
$expected_document = $individual_response
->getResponseData();
$self_link = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), [
static::$entityTypeId => $this->entity
->uuid(),
], [
'query' => [
'include' => implode(',', $include_paths),
],
])
->setAbsolute()
->toString();
$expected_document['links']['self'] = $self_link;
if (empty($include_paths)) {
return (new ResourceResponse($expected_document))
->addCacheableDependency($individual_response
->getCacheableMetadata());
}
$resource_data = $this
->getExpectedIncludedResourceResponse($include_paths, $request_options);
$resource_document = $resource_data
->getResponseData();
if (isset($resource_document['data'])) {
foreach ($resource_document['data'] as $related_resource) {
if (empty($expected_document['included']) || !static::collectionHasResourceIdentifier($related_resource, $expected_document['included'])) {
$expected_document['included'][] = $related_resource;
}
}
}
if (!empty($resource_document['meta']['errors'])) {
foreach ($resource_document['meta']['errors'] as $error) {
if (strpos($error['detail'], 'The current user is not allowed to view this relationship.') !== 0) {
$expected_document['meta']['errors'][] = $error;
}
}
}
return $expected_response = (new ResourceResponse($expected_document))
->addCacheableDependency($individual_response
->getCacheableMetadata())
->addCacheableDependency($resource_data
->getCacheableMetadata());
}
public function testPostIndividual() {
if ($this->entity instanceof ConfigEntityInterface) {
$this
->assertTrue(TRUE, 'POSTing config entities is not yet supported.');
return;
}
$unparseable_request_body = '!{>}<';
$parseable_valid_request_body = Json::encode($this
->getPostDocument());
$parseable_invalid_request_body_missing_type = Json::encode($this
->removeResourceTypeFromDocument($this
->getPostDocument(), 'type'));
$parseable_invalid_request_body = Json::encode($this
->makeNormalizationInvalid($this
->getPostDocument(), 'label'));
$parseable_invalid_request_body_2 = Json::encode(NestedArray::mergeDeep([
'data' => [
'id' => $this
->randomMachineName(129),
],
], $this
->getPostDocument()));
$parseable_invalid_request_body_3 = Json::encode(NestedArray::mergeDeep([
'data' => [
'attributes' => [
'field_rest_test' => $this
->randomString(),
],
],
], $this
->getPostDocument()));
$parseable_invalid_request_body_4 = Json::encode(NestedArray::mergeDeep([
'data' => [
'attributes' => [
'field_nonexistent' => $this
->randomString(),
],
],
], $this
->getPostDocument()));
$url = Url::fromRoute(sprintf('jsonapi.%s.collection', static::$resourceTypeName));
$request_options = [];
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
$request_options = NestedArray::mergeDeep($request_options, $this
->getAuthenticationRequestOptions());
$request_options[RequestOptions::HEADERS]['Content-Type'] = '';
$response = $this
->request('POST', $url, $request_options);
$this
->assertResourceErrorResponse(400, 'Empty request body.', $response);
$request_options[RequestOptions::BODY] = $unparseable_request_body;
$response = $this
->request('POST', $url, $request_options);
$this
->assertResourceErrorResponse(400, 'Syntax error', $response);
$request_options[RequestOptions::BODY] = $parseable_invalid_request_body;
$response = $this
->request('POST', $url, $request_options);
$reason = $this
->getExpectedUnauthorizedAccessMessage('POST');
$message = trim("The current user is not allowed to POST the selected resource. {$reason}");
$this
->assertResourceErrorResponse(403, $message, $response, '/data');
$this
->setUpAuthorization('POST');
$request_options[RequestOptions::BODY] = $parseable_invalid_request_body_missing_type;
$response = $this
->request('POST', $url, $request_options);
$this
->assertResourceErrorResponse(400, 'Resource object must include a "type".', $response);
$request_options[RequestOptions::BODY] = $parseable_invalid_request_body;
$response = $this
->request('POST', $url, $request_options);
$label_field = $this->entity
->getEntityType()
->hasKey('label') ? $this->entity
->getEntityType()
->getKey('label') : static::$labelFieldName;
$label_field_capitalized = $this->entity
->getFieldDefinition($label_field)
->getLabel();
$expected_document = [
'errors' => [
[
'title' => 'Unprocessable Entity',
'status' => 422,
'detail' => "{$label_field}: {$label_field_capitalized}: this field cannot hold more than 1 values.",
'code' => 0,
'source' => [
'pointer' => '/data/attributes/' . $label_field,
],
],
],
];
$this
->assertResourceResponse(422, $expected_document, $response);
$request_options[RequestOptions::BODY] = $parseable_invalid_request_body_2;
if ($this->entity
->getEntityType()
->hasKey('uuid')) {
$response = $this
->request('POST', $url, $request_options);
$this
->assertResourceErrorResponse(422, "IDs should be properly generated and formatted UUIDs as described in RFC 4122.", $response);
}
$request_options[RequestOptions::BODY] = $parseable_invalid_request_body_3;
$response = $this
->request('POST', $url, $request_options);
$this
->assertResourceErrorResponse(403, "The current user is not allowed to POST the selected field (field_rest_test).", $response, '/data/attributes/field_rest_test');
$request_options[RequestOptions::BODY] = $parseable_invalid_request_body_4;
$response = $this
->request('POST', $url, $request_options);
$this
->assertResourceErrorResponse(422, sprintf("The attribute field_nonexistent does not exist on the %s resource type.", static::$resourceTypeName), $response);
$request_options[RequestOptions::BODY] = $parseable_valid_request_body;
$request_options[RequestOptions::HEADERS]['Content-Type'] = 'application/vnd.api+json';
$response = $this
->request('POST', $url, $request_options);
$this
->assertResourceResponse(201, FALSE, $response);
$this
->assertFalse($response
->hasHeader('X-Drupal-Cache'));
if (get_class($this->entityStorage) !== ContentEntityNullStorage::class) {
$uuid = $this->entityStorage
->load(static::$firstCreatedEntityId)
->uuid();
$location = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), [
static::$entityTypeId => $uuid,
])
->setAbsolute(TRUE)
->toString();
$this
->assertSame([
$location,
], $response
->getHeader('Location'));
$created_entity = $this->entityStorage
->loadUnchanged(static::$firstCreatedEntityId);
$created_entity_document = $this
->normalize($created_entity, $url);
if (static::$entityTypeId !== 'taxonomy_term') {
$decoded_response_body = Json::decode((string) $response
->getBody());
$this
->assertSame($created_entity_document, $decoded_response_body);
}
foreach ($this
->getPostDocument()['data']['attributes'] as $field_name => $field_normalization) {
if (is_array($field_normalization)) {
$this
->assertArraySubset($field_normalization, $created_entity_document['data']['attributes'][$field_name]);
}
else {
$this
->assertSame($field_normalization, $created_entity_document['data']['attributes'][$field_name]);
}
}
if (isset($this
->getPostDocument()['data']['relationships'])) {
foreach ($this
->getPostDocument()['data']['relationships'] as $field_name => $relationship_field_normalization) {
static::recursiveKsort($relationship_field_normalization);
static::recursiveKsort($created_entity_document['data']['relationships'][$field_name]);
$this
->assertSame($relationship_field_normalization, array_diff_key($created_entity_document['data']['relationships'][$field_name], [
'links' => TRUE,
]));
}
}
}
else {
$this
->assertFalse($response
->hasHeader('Location'));
}
if (get_class($this->entityStorage) !== ContentEntityNullStorage::class) {
$this->entityStorage
->load(static::$firstCreatedEntityId)
->delete();
}
$response = $this
->request('POST', $url, $request_options);
$this
->assertResourceResponse(201, FALSE, $response);
$this
->assertFalse($response
->hasHeader('X-Drupal-Cache'));
if ($this->entity
->getEntityType()
->getStorageClass() !== ContentEntityNullStorage::class && $this->entity
->getEntityType()
->hasKey('uuid')) {
$uuid = $this->entityStorage
->load(static::$secondCreatedEntityId)
->uuid();
$location = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), [
static::$entityTypeId => $uuid,
])
->setAbsolute(TRUE)
->toString();
$this
->assertSame([
$location,
], $response
->getHeader('Location'));
$doc = $this
->getModifiedEntityForPostTesting();
$doc['data']['id'] = $uuid;
$doc['data']['attributes'][$label_field] = [
[
'value' => $this
->randomMachineName(),
],
];
$request_options[RequestOptions::BODY] = Json::encode($doc);
$response = $this
->request('POST', $url, $request_options);
$this
->assertResourceErrorResponse(409, 'Conflict: Entity already exists.', $response);
$doc = $this
->getModifiedEntityForPostTesting();
$new_uuid = \Drupal::service('uuid')
->generate();
$doc['data']['id'] = $new_uuid;
$doc['data']['attributes'][$label_field] = [
[
'value' => $this
->randomMachineName(),
],
];
$request_options[RequestOptions::BODY] = Json::encode($doc);
$response = $this
->request('POST', $url, $request_options);
$this
->assertResourceResponse(201, FALSE, $response);
$entities = $this->entityStorage
->loadByProperties([
'uuid' => $new_uuid,
]);
$new_entity = reset($entities);
$this
->assertNotNull($new_entity);
$new_entity
->delete();
}
else {
$this
->assertFalse($response
->hasHeader('Location'));
}
}
public function testPatchIndividual() {
if ($this->entity instanceof ConfigEntityInterface) {
$this
->assertTrue(TRUE, 'PATCHing config entities is not yet supported.');
return;
}
$this->anotherEntity = $this
->createAnotherEntity('dupe');
$unparseable_request_body = '!{>}<';
$parseable_valid_request_body = Json::encode($this
->getPatchDocument());
$parseable_invalid_request_body = Json::encode($this
->makeNormalizationInvalid($this
->getPatchDocument(), 'label'));
$parseable_invalid_request_body_2 = Json::encode(NestedArray::mergeDeep([
'data' => [
'attributes' => [
'field_rest_test' => $this
->randomString(),
],
],
], $this
->getPatchDocument()));
$parseable_invalid_request_body_3 = Json::encode(NestedArray::mergeDeep([
'data' => [
'attributes' => [
'field_rest_test' => $this->entity
->get('field_rest_test')
->getValue(),
],
],
], $this
->getPatchDocument()));
$parseable_invalid_request_body_4 = Json::encode(NestedArray::mergeDeep([
'data' => [
'attributes' => [
'field_nonexistent' => $this
->randomString(),
],
],
], $this
->getPatchDocument()));
$url = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), [
static::$entityTypeId => $this->entity
->uuid(),
]);
$request_options = [];
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
$request_options = NestedArray::mergeDeep($request_options, $this
->getAuthenticationRequestOptions());
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceErrorResponse(400, 'Empty request body.', $response);
$request_options[RequestOptions::BODY] = $unparseable_request_body;
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceErrorResponse(400, 'Syntax error', $response);
$request_options[RequestOptions::BODY] = $parseable_invalid_request_body;
$response = $this
->request('PATCH', $url, $request_options);
$reason = $this
->getExpectedUnauthorizedAccessMessage('PATCH');
$expected_document = [
'errors' => [
[
'title' => 'Forbidden',
'status' => 403,
'detail' => "The current user is not allowed to PATCH the selected resource." . (strlen($reason) ? ' ' . $reason : ''),
'links' => [
'info' => HttpExceptionNormalizer::getInfoUrl(403),
],
'code' => 0,
'id' => '/' . static::$resourceTypeName . '/' . $this->entity
->uuid(),
'source' => [
'pointer' => '/data',
],
],
],
];
$this
->assertResourceResponse(403, $expected_document, $response);
$this
->setUpAuthorization('PATCH');
$response = $this
->request('PATCH', $url, $request_options);
$label_field = $this->entity
->getEntityType()
->hasKey('label') ? $this->entity
->getEntityType()
->getKey('label') : static::$labelFieldName;
$label_field_capitalized = $this->entity
->getFieldDefinition($label_field)
->getLabel();
$expected_document = [
'errors' => [
[
'title' => 'Unprocessable Entity',
'status' => 422,
'detail' => "{$label_field}: {$label_field_capitalized}: this field cannot hold more than 1 values.",
'code' => 0,
'source' => [
'pointer' => '/data/attributes/' . $label_field,
],
],
],
];
$this
->assertResourceResponse(422, $expected_document, $response);
$request_options[RequestOptions::BODY] = $parseable_invalid_request_body_2;
$response = $this
->request('PATCH', $url, $request_options);
$expected_document = [
'errors' => [
[
'title' => 'Forbidden',
'status' => 403,
'detail' => "The current user is not allowed to PATCH the selected field (field_rest_test).",
'links' => [
'info' => HttpExceptionNormalizer::getInfoUrl(403),
],
'code' => 0,
'id' => '/' . static::$resourceTypeName . '/' . $this->entity
->uuid(),
'source' => [
'pointer' => '/data/attributes/field_rest_test',
],
],
],
];
$this
->assertResourceResponse(403, $expected_document, $response);
$request_options[RequestOptions::BODY] = Json::encode($this
->makeNormalizationInvalid($this
->getPatchDocument(), 'id'));
$response = $this
->request('PATCH', $url, $request_options);
$id_field_name = $this->entity
->getEntityType()
->getKey('id');
$expected_document = [
'errors' => [
[
'title' => 'Forbidden',
'status' => 403,
'detail' => "The current user is not allowed to PATCH the selected field ({$id_field_name}). The entity ID cannot be changed.",
'links' => [
'info' => HttpExceptionNormalizer::getInfoUrl(403),
],
'code' => 0,
'id' => '/' . static::$resourceTypeName . '/' . $this->entity
->uuid(),
'source' => [
'pointer' => '/data/attributes/' . $id_field_name,
],
],
],
];
if (floatval(\Drupal::VERSION) < 8.6) {
$expected_document['errors'][0]['detail'] = "The current user is not allowed to PATCH the selected field ({$id_field_name}). The entity ID cannot be changed";
}
$this
->assertResourceResponse(403, $expected_document, $response);
if ($this->entity
->getEntityType()
->hasKey('uuid')) {
$request_options[RequestOptions::BODY] = Json::encode($this
->makeNormalizationInvalid($this
->getPatchDocument(), 'uuid'));
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceErrorResponse(400, sprintf("The selected entity (%s) does not match the ID in the payload (%s).", $this->entity
->uuid(), $this->anotherEntity
->uuid()), $response);
}
$request_options[RequestOptions::BODY] = $parseable_invalid_request_body_3;
$response = $this
->request('PATCH', $url, $request_options);
$expected_document = [
'errors' => [
[
'title' => 'Forbidden',
'status' => 403,
'detail' => "The current user is not allowed to PATCH the selected field (field_rest_test).",
'links' => [
'info' => HttpExceptionNormalizer::getInfoUrl(403),
],
'code' => 0,
'id' => '/' . static::$resourceTypeName . '/' . $this->entity
->uuid(),
'source' => [
'pointer' => '/data/attributes/field_rest_test',
],
],
],
];
$this
->assertResourceResponse(403, $expected_document, $response);
list($modified_entity, $original_values) = static::getModifiedEntityForPatchTesting($this->entity);
foreach (static::$patchProtectedFieldNames as $patch_protected_field_name => $reason) {
$request_options[RequestOptions::BODY] = Json::encode($this
->normalize($modified_entity, $url));
$response = $this
->request('PATCH', $url, $request_options);
$expected_document = [
'errors' => [
[
'title' => 'Forbidden',
'status' => 403,
'detail' => "The current user is not allowed to PATCH the selected field (" . $patch_protected_field_name . ")." . ($reason !== NULL ? ' ' . $reason : ''),
'links' => [
'info' => HttpExceptionNormalizer::getInfoUrl(403),
],
'code' => 0,
'id' => '/' . static::$resourceTypeName . '/' . $this->entity
->uuid(),
'source' => [
'pointer' => '/data/attributes/' . $patch_protected_field_name,
],
],
],
];
$this
->assertResourceResponse(403, $expected_document, $response);
$modified_entity
->get($patch_protected_field_name)
->setValue($original_values[$patch_protected_field_name]);
}
$request_options[RequestOptions::BODY] = $parseable_invalid_request_body_4;
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceErrorResponse(422, sprintf("The attribute field_nonexistent does not exist on the %s resource type.", static::$resourceTypeName), $response);
$valid_request_body = NestedArray::mergeDeep($this
->normalize($this->entity, $url), $this
->getPatchDocument());
$request_options[RequestOptions::BODY] = Json::encode($valid_request_body);
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceResponse(200, FALSE, $response);
$request_options[RequestOptions::BODY] = $parseable_valid_request_body;
$request_options[RequestOptions::HEADERS]['Content-Type'] = 'application/vnd.api+json';
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceResponse(200, FALSE, $response);
$this
->assertFalse($response
->hasHeader('X-Drupal-Cache'));
$updated_entity = $this->entityStorage
->loadUnchanged($this->entity
->id());
$updated_entity_document = $this
->normalize($updated_entity, $url);
$this
->assertSame($updated_entity_document, Json::decode((string) $response
->getBody()));
foreach ($this
->getPatchDocument() as $field_name => $field_normalization) {
if ($updated_entity
->hasField($field_name)) {
$this
->assertArraySubset($field_normalization, $updated_entity
->get($field_name)
->getValue(), TRUE);
}
}
$this
->assertSame('All the faith he had had had had no effect on the outcome of his life.', $updated_entity
->get('field_rest_test')->value);
if (floatval(\Drupal::VERSION) < 8.5) {
return;
}
$doc_multi_value_tests = $this
->getPatchDocument();
$doc_multi_value_tests['data']['attributes']['field_rest_test_multivalue'] = $this->entity
->get('field_rest_test_multivalue')
->getValue();
$doc_remove_item = $doc_multi_value_tests;
unset($doc_remove_item['data']['attributes']['field_rest_test_multivalue'][0]);
$request_options[RequestOptions::BODY] = Json::encode($doc_remove_item, 'api_json');
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceResponse(200, FALSE, $response);
$this
->assertSame([
0 => [
'value' => 'Two',
],
], $this->entityStorage
->loadUnchanged($this->entity
->id())
->get('field_rest_test_multivalue')
->getValue());
$doc_add_items = $doc_multi_value_tests;
$doc_add_items['data']['attributes']['field_rest_test_multivalue'][2] = [
'value' => 'Three',
];
$request_options[RequestOptions::BODY] = Json::encode($doc_add_items);
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceResponse(200, FALSE, $response);
$expected_document = [
0 => [
'value' => 'One',
],
1 => [
'value' => 'Two',
],
2 => [
'value' => 'Three',
],
];
$this
->assertSame($expected_document, $this->entityStorage
->loadUnchanged($this->entity
->id())
->get('field_rest_test_multivalue')
->getValue());
}
public function testDeleteIndividual() {
if ($this->entity instanceof ConfigEntityInterface) {
$this
->assertTrue(TRUE, 'DELETEing config entities is not yet supported.');
return;
}
$url = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), [
static::$entityTypeId => $this->entity
->uuid(),
]);
$request_options = [];
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
$request_options = NestedArray::mergeDeep($request_options, $this
->getAuthenticationRequestOptions());
$response = $this
->request('DELETE', $url, $request_options);
$reason = $this
->getExpectedUnauthorizedAccessMessage('DELETE');
$expected_document = [
'errors' => [
[
'title' => 'Forbidden',
'status' => 403,
'detail' => "The current user is not allowed to DELETE the selected resource." . (strlen($reason) ? ' ' . $reason : ''),
'links' => [
'info' => HttpExceptionNormalizer::getInfoUrl(403),
],
'code' => 0,
'id' => '/' . static::$resourceTypeName . '/' . $this->entity
->uuid(),
'source' => [
'pointer' => '/data',
],
],
],
];
$this
->assertResourceResponse(403, $expected_document, $response);
$this
->setUpAuthorization('DELETE');
$response = $this
->request('DELETE', $url, $request_options);
$this
->assertResourceResponse(204, NULL, $response);
}
protected static function recursiveKsort(array &$array) {
ksort($array);
foreach ($array as $key => &$value) {
if (is_array($value)) {
static::recursiveKsort($value);
}
}
}
protected static function sortErrors(array &$errors) {
usort($errors, function ($a, $b) {
return strcmp($a['id'], $b['id']);
});
}
protected function getAuthenticationRequestOptions() {
return [
'headers' => [
'Authorization' => 'Basic ' . base64_encode($this->account->name->value . ':' . $this->account->passRaw),
],
];
}
protected static function getModifiedEntityForPatchTesting(EntityInterface $entity) {
$modified_entity = clone $entity;
$original_values = [];
foreach (array_keys(static::$patchProtectedFieldNames) as $field_name) {
$field = $modified_entity
->get($field_name);
$original_values[$field_name] = $field
->getValue();
switch ($field
->getItemDefinition()
->getClass()) {
case EntityReferenceItem::class:
$field
->setValue([
'target_id' => 99999,
]);
break;
case BooleanItem::class:
$field->value = (int) $field->value === 1 ? '0' : '1';
break;
case PathItem::class:
$field->alias = str_replace(' ', '-', strtolower((new Random())
->sentences(3)));
break;
default:
$original_field = clone $field;
while ($field
->equals($original_field)) {
$field
->generateSampleItems();
}
break;
}
}
return [
$modified_entity,
$original_values,
];
}
protected function getModifiedEntityForPostTesting() {
$document = $this
->getPostDocument();
foreach (static::$uniqueFieldNames as $field_name) {
$field_definition = $this->entity
->getFieldDefinition($field_name);
$field_type_class = $field_definition
->getItemDefinition()
->getClass();
$document['data']['attributes'][$field_name] = $field_type_class::generateSampleValue($field_definition);
}
return $document;
}
protected function doTestSparseFieldSets(Url $url, array $request_options) {
$field_sets = $this
->getSparseFieldSets();
$expected_cacheability = new CacheableMetadata();
foreach ($field_sets as $type => $field_set) {
if ($type === 'all') {
assert($this
->getExpectedCacheTags($field_set) === $this
->getExpectedCacheTags());
assert($this
->getExpectedCacheContexts($field_set) === $this
->getExpectedCacheContexts());
}
$query = [
'fields[' . static::$resourceTypeName . ']' => implode(',', $field_set),
];
$expected_document = $this
->getExpectedDocument();
$expected_cacheability
->setCacheTags($this
->getExpectedCacheTags($field_set));
$expected_cacheability
->setCacheContexts($this
->getExpectedCacheContexts($field_set));
if (strpos($type, 'nested') === 0) {
$this
->grantPermissionsToTestedRole([
'access user profiles',
]);
$query['fields[user--user]'] = implode(',', $field_set);
$query['include'] = 'uid';
$owner = $this->entity
->getOwner();
$owner_resource = static::toResourceIdentifier($owner);
foreach ($field_set as $field_name) {
$owner_resource['attributes'][$field_name] = $owner
->get($field_name)[0]
->get('value')
->getCastedValue();
}
$owner_resource['links']['self'] = static::getResourceLink($owner_resource);
$expected_document['included'] = [
$owner_resource,
];
$expected_cacheability
->addCacheableDependency($owner);
$expected_cacheability
->addCacheableDependency(static::entityAccess($owner, 'view', $this->account));
}
foreach ([
'attributes',
'relationships',
] as $member) {
if (!empty($expected_document['data'][$member])) {
$remaining = array_intersect_key($expected_document['data'][$member], array_flip($field_set));
if (empty($remaining)) {
unset($expected_document['data'][$member]);
}
else {
$expected_document['data'][$member] = $remaining;
}
}
}
$url
->setOption('query', $query);
$expected_document['links']['self'] = $url
->setAbsolute()
->toString();
$response = $this
->request('GET', $url, $request_options);
$this
->assertResourceResponse(200, $expected_document, $response, $expected_cacheability
->getCacheTags(), $expected_cacheability
->getCacheContexts(), FALSE, 'MISS');
}
$response = $this
->request('GET', $url, $request_options);
$this
->assertResourceResponse(200, FALSE, $response, $expected_cacheability
->getCacheTags(), $expected_cacheability
->getCacheContexts(), FALSE, 'HIT');
}
protected function doTestIncluded(Url $url, array $request_options) {
$relationship_field_names = $this
->getRelationshipFieldNames($this->entity);
if (empty($relationship_field_names)) {
return;
}
$field_sets = [
'empty' => [],
'all' => $relationship_field_names,
];
if (count($relationship_field_names) > 1) {
$about_half_the_fields = floor(count($relationship_field_names) / 2);
$field_sets['some'] = array_slice($relationship_field_names, $about_half_the_fields);
}
$nested_includes = $this
->getNestedIncludePaths();
if (!empty($nested_includes)) {
$field_sets['nested'] = $nested_includes;
}
foreach ($field_sets as $type => $included_paths) {
foreach (array_intersect_key(static::getIncludePermissions(), array_flip($included_paths)) as $permissions) {
$this
->grantPermissionsToTestedRole($permissions);
}
$expected_response = $this
->getExpectedIncludeResponse($included_paths, $request_options);
$query = [
'include' => implode(',', $included_paths),
];
$url
->setOption('query', $query);
$actual_response = $this
->request('GET', $url, $request_options);
$response_document = Json::decode((string) $actual_response
->getBody());
$expected_document = $expected_response
->getResponseData();
$this
->assertSameDocument($expected_document, $response_document);
}
}
protected static function decorateExpectedResponseForIncludedFields(ResourceResponse $expected_response, array $related_responses) {
$expected_document = $expected_response
->getResponseData();
$expected_cacheability = $expected_response
->getCacheableMetadata();
foreach ($related_responses as $related_response) {
$related_document = $related_response
->getResponseData();
$expected_cacheability
->addCacheableDependency($related_response
->getCacheableMetadata());
if (!empty($related_document['errors'])) {
foreach ($related_document['errors'] as $error) {
if (strpos($error['detail'], 'The current user is not allowed to view this relationship.') !== 0) {
unset($error['source']['pointer']);
$expected_document['meta']['errors'][] = $error;
}
}
}
elseif (isset($related_document['data'])) {
$related_data = $related_document['data'];
$related_resources = static::isResourceIdentifier($related_data) ? [
$related_data,
] : $related_data;
foreach ($related_resources as $related_resource) {
if (empty($expected_document['included']) || !static::collectionHasResourceIdentifier($related_resource, $expected_document['included'])) {
$expected_document['included'][] = $related_resource;
}
}
}
}
return (new ResourceResponse($expected_document))
->addCacheableDependency($expected_cacheability);
}
protected function getExpectedGetIndividualResourceResponse($status_code = 200) {
$resource_response = new ResourceResponse($this
->getExpectedDocument(), $status_code);
$cacheability = new CacheableMetadata();
$cacheability
->setCacheContexts($this
->getExpectedCacheContexts());
$cacheability
->setCacheTags($this
->getExpectedCacheTags());
return $resource_response
->addCacheableDependency($cacheability);
}
protected function getSparseFieldSets() {
$field_names = array_keys($this->entity
->toArray());
$field_sets = [
'empty' => [],
'some' => array_slice($field_names, floor(count($field_names) / 2)),
'all' => $field_names,
];
if ($this->entity instanceof EntityOwnerInterface) {
$field_sets['nested_empty_fieldset'] = $field_sets['empty'];
$field_sets['nested_fieldset_with_owner_fieldset'] = [
'name',
'created',
];
}
return $field_sets;
}
protected function getRelationshipFieldNames(EntityInterface $entity = NULL) {
$entity = $entity ?: $this->entity;
$fields = $entity instanceof ContentEntityInterface ? iterator_to_array($entity) : [];
return array_reduce($fields, function ($field_names, $field) {
if (static::isReferenceFieldDefinition($field
->getFieldDefinition())) {
$field_names[] = $field
->getName();
}
return $field_names;
}, []);
}
protected static function getIncludePermissions() {
return [];
}
protected static function entityAccess(EntityInterface $entity, $operation, AccountInterface $account) {
\Drupal::entityTypeManager()
->getAccessControlHandler($entity
->getEntityTypeId())
->resetCache();
return $entity
->access($operation, $account, TRUE);
}
protected static function entityFieldAccess(EntityInterface $entity, $field_name, $operation, AccountInterface $account) {
$entity_access = static::entityAccess($entity, $operation, $account);
$field_access = $entity->{$field_name}
->access($operation, $account, TRUE);
return $entity_access
->andIf($field_access);
}
protected function getNestedIncludePaths($depth = 3) {
$get_nested_relationship_field_names = function (EntityInterface $entity, $depth, $path = "") use (&$get_nested_relationship_field_names) {
$relationship_field_names = $this
->getRelationshipFieldNames($entity);
if ($depth > 0) {
$paths = $path ? [
$path,
] : [];
foreach ($relationship_field_names as $field_name) {
$next = $path ? "{$path}.{$field_name}" : $field_name;
if ($target_entity = $entity->{$field_name}->entity) {
$deep = $get_nested_relationship_field_names($target_entity, $depth - 1, $next);
$paths = array_merge($paths, $deep);
}
else {
$paths[] = $next;
}
}
return $paths;
}
return array_map(function ($target_name) use ($path) {
return "{$path}.{$target_name}";
}, $relationship_field_names);
};
return $get_nested_relationship_field_names($this->entity, $depth);
}
protected static function isReferenceFieldDefinition(FieldDefinitionInterface $field_definition) {
$item_definition = $field_definition
->getItemDefinition();
$main_property = $item_definition
->getMainPropertyName();
$property_definition = $item_definition
->getPropertyDefinition($main_property);
return $property_definition instanceof DataReferenceTargetDefinition;
}
}