Initial Drupal 11 with DDEV setup
This commit is contained in:
121
web/core/modules/jsonapi/tests/src/Functional/ActionTest.php
Normal file
121
web/core/modules/jsonapi/tests/src/Functional/ActionTest.php
Normal file
@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\system\Entity\Action;
|
||||
use Drupal\user\RoleInterface;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "Action" config entity type.
|
||||
*
|
||||
* @group Action
|
||||
*/
|
||||
class ActionTest extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'action';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'action--action';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\system\ActionConfigEntityInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['administer actions']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
$action = Action::create([
|
||||
'id' => 'user_add_role_action.' . RoleInterface::ANONYMOUS_ID,
|
||||
'type' => 'user',
|
||||
'label' => 'Add the anonymous role to the selected users',
|
||||
'configuration' => [
|
||||
'rid' => RoleInterface::ANONYMOUS_ID,
|
||||
],
|
||||
'plugin' => 'user_add_role_action',
|
||||
]);
|
||||
$action->save();
|
||||
|
||||
return $action;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/action/action/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'action--action',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'configuration' => [
|
||||
'rid' => 'anonymous',
|
||||
],
|
||||
'dependencies' => [
|
||||
'config' => ['user.role.anonymous'],
|
||||
'module' => ['user'],
|
||||
],
|
||||
'label' => 'Add the anonymous role to the selected users',
|
||||
'langcode' => 'en',
|
||||
'plugin' => 'user_add_role_action',
|
||||
'status' => TRUE,
|
||||
'action_type' => 'user',
|
||||
'drupal_internal__id' => 'user_add_role_action.anonymous',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
// @todo Update in https://www.drupal.org/node/2300677.
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Core\Field\Entity\BaseFieldOverride;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "BaseFieldOverride" config entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class BaseFieldOverrideTest extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['field', 'node', 'field_ui'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'base_field_override';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'base_field_override--base_field_override';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\Core\Field\Entity\BaseFieldOverride
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['administer node fields']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
$camelids = NodeType::create([
|
||||
'name' => 'Camelids',
|
||||
'type' => 'camelids',
|
||||
]);
|
||||
$camelids->save();
|
||||
|
||||
$entity = BaseFieldOverride::create([
|
||||
'field_name' => 'promote',
|
||||
'entity_type' => 'node',
|
||||
'bundle' => 'camelids',
|
||||
'label' => 'Promote to front page',
|
||||
]);
|
||||
$entity->save();
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/base_field_override/base_field_override/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'base_field_override--base_field_override',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'bundle' => 'camelids',
|
||||
'default_value' => [],
|
||||
'default_value_callback' => '',
|
||||
'dependencies' => [
|
||||
'config' => [
|
||||
'node.type.camelids',
|
||||
],
|
||||
],
|
||||
'description' => '',
|
||||
'entity_type' => 'node',
|
||||
'field_name' => 'promote',
|
||||
'field_type' => 'boolean',
|
||||
'label' => 'Promote to front page',
|
||||
'langcode' => 'en',
|
||||
'required' => FALSE,
|
||||
'settings' => [
|
||||
'on_label' => 'On',
|
||||
'off_label' => 'Off',
|
||||
],
|
||||
'status' => TRUE,
|
||||
'translatable' => TRUE,
|
||||
'drupal_internal__id' => 'node.camelids.promote',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
// @todo Update in https://www.drupal.org/node/2300677.
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessMessage($method): string {
|
||||
return "The 'administer node fields' permission is required.";
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createAnotherEntity($key) {
|
||||
$entity = BaseFieldOverride::create([
|
||||
'field_name' => 'status',
|
||||
'entity_type' => 'node',
|
||||
'bundle' => 'camelids',
|
||||
'label' => 'Published',
|
||||
]);
|
||||
$entity->save();
|
||||
return $entity;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,268 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\block_content\Entity\BlockContent;
|
||||
use Drupal\block_content\Entity\BlockContentType;
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\Tests\jsonapi\Traits\CommonCollectionFilterAccessTestPatternsTrait;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "BlockContent" content entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class BlockContentTest extends ResourceTestBase {
|
||||
|
||||
use CommonCollectionFilterAccessTestPatternsTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['block_content'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'block_content';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'block_content--basic';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeIsVersionable = TRUE;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $newRevisionsShouldBeAutomatic = TRUE;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\block_content\BlockContentInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $patchProtectedFieldNames = [
|
||||
'changed' => NULL,
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
$this->grantPermissionsToTestedRole([
|
||||
'access block library',
|
||||
]);
|
||||
break;
|
||||
|
||||
case 'PATCH':
|
||||
$this->grantPermissionsToTestedRole([
|
||||
'administer block types',
|
||||
'administer block content',
|
||||
]);
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
$this->grantPermissionsToTestedRole(['create basic block content']);
|
||||
break;
|
||||
|
||||
case 'DELETE':
|
||||
$this->grantPermissionsToTestedRole(['delete any basic block content']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpRevisionAuthorization($method): void {
|
||||
parent::setUpRevisionAuthorization($method);
|
||||
$this->grantPermissionsToTestedRole(['view any basic block content history']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function createEntity() {
|
||||
if (!BlockContentType::load('basic')) {
|
||||
$block_content_type = BlockContentType::create([
|
||||
'id' => 'basic',
|
||||
'label' => 'basic',
|
||||
'revision' => TRUE,
|
||||
]);
|
||||
$block_content_type->save();
|
||||
block_content_add_body_field($block_content_type->id());
|
||||
}
|
||||
|
||||
// Create a "Llama" content block.
|
||||
$block_content = BlockContent::create([
|
||||
'info' => 'Llama',
|
||||
'type' => 'basic',
|
||||
'body' => [
|
||||
'value' => 'The name "llama" was adopted by European settlers from native Peruvians.',
|
||||
'format' => 'plain_text',
|
||||
],
|
||||
])
|
||||
->setUnpublished();
|
||||
$block_content->save();
|
||||
return $block_content;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$base_url = Url::fromUri('base:/jsonapi/block_content/basic/' . $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' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $base_url->toString()],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'block_content--basic',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url->toString()],
|
||||
],
|
||||
'attributes' => [
|
||||
'body' => [
|
||||
'value' => 'The name "llama" was adopted by European settlers from native Peruvians.',
|
||||
'format' => 'plain_text',
|
||||
'summary' => NULL,
|
||||
'processed' => "<p>The name "llama" was adopted by European settlers from native Peruvians.</p>\n",
|
||||
],
|
||||
'changed' => (new \DateTime())->setTimestamp($this->entity->getChangedTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
|
||||
'info' => 'Llama',
|
||||
'revision_created' => (new \DateTime())->setTimestamp((int) $this->entity->getRevisionCreationTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
|
||||
'revision_translation_affected' => TRUE,
|
||||
'status' => FALSE,
|
||||
'langcode' => 'en',
|
||||
'default_langcode' => TRUE,
|
||||
'drupal_internal__id' => 1,
|
||||
'drupal_internal__revision_id' => 1,
|
||||
'reusable' => TRUE,
|
||||
],
|
||||
'relationships' => [
|
||||
'block_content_type' => [
|
||||
'data' => [
|
||||
'id' => BlockContentType::load('basic')->uuid(),
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => 'basic',
|
||||
],
|
||||
'type' => 'block_content_type--block_content_type',
|
||||
],
|
||||
'links' => [
|
||||
'related' => ['href' => $base_url->toString() . '/block_content_type' . $version_query_string],
|
||||
'self' => ['href' => $base_url->toString() . '/relationships/block_content_type' . $version_query_string],
|
||||
],
|
||||
],
|
||||
'revision_user' => [
|
||||
'data' => NULL,
|
||||
'links' => [
|
||||
'related' => ['href' => $base_url->toString() . '/revision_user' . $version_query_string],
|
||||
'self' => ['href' => $base_url->toString() . '/relationships/revision_user' . $version_query_string],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
return [
|
||||
'data' => [
|
||||
'type' => 'block_content--basic',
|
||||
'attributes' => [
|
||||
'info' => 'Drama llama',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessMessage($method) {
|
||||
return match ($method) {
|
||||
'GET' => "The 'access block library' permission is required.",
|
||||
'PATCH' => "The 'edit any basic block content' permission is required.",
|
||||
'POST' => "The following permissions are required: 'create basic block content' OR 'administer block content'.",
|
||||
'DELETE' => "The 'delete any basic block content' permission is required.",
|
||||
default => parent::getExpectedUnauthorizedAccessMessage($method),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessCacheability() {
|
||||
// @see \Drupal\block_content\BlockContentAccessControlHandler()
|
||||
return parent::getExpectedUnauthorizedAccessCacheability()
|
||||
->addCacheTags(['block_content:1']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedCacheTags(?array $sparse_fieldset = NULL) {
|
||||
$tags = parent::getExpectedCacheTags($sparse_fieldset);
|
||||
if ($sparse_fieldset === NULL || in_array('body', $sparse_fieldset)) {
|
||||
$tags = Cache::mergeTags($tags, ['config:filter.format.plain_text']);
|
||||
}
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedCacheContexts(?array $sparse_fieldset = NULL) {
|
||||
$contexts = parent::getExpectedCacheContexts($sparse_fieldset);
|
||||
if ($sparse_fieldset === NULL || in_array('body', $sparse_fieldset)) {
|
||||
$contexts = Cache::mergeContexts($contexts, ['languages:language_interface', 'theme']);
|
||||
}
|
||||
return $contexts;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function testCollectionFilterAccess(): void {
|
||||
$this->entity->setPublished()->save();
|
||||
$this->doTestCollectionFilterAccessForPublishableEntities('info', NULL, 'administer block content');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\block_content\Entity\BlockContentType;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "BlockContentType" config entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class BlockContentTypeTest extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['block_content'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'block_content_type';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'block_content_type--block_content_type';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\block_content\BlockContentTypeInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['administer block types']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
$block_content_type = BlockContentType::create([
|
||||
'id' => 'pascal',
|
||||
'label' => 'Pascal',
|
||||
'revision' => FALSE,
|
||||
'description' => 'Provides a competitive alternative to the "basic" type',
|
||||
]);
|
||||
|
||||
$block_content_type->save();
|
||||
|
||||
return $block_content_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/block_content_type/block_content_type/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'block_content_type--block_content_type',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'dependencies' => [],
|
||||
'description' => 'Provides a competitive alternative to the "basic" type',
|
||||
'label' => 'Pascal',
|
||||
'langcode' => 'en',
|
||||
'revision' => FALSE,
|
||||
'status' => TRUE,
|
||||
'drupal_internal__id' => 'pascal',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
// @todo Update in https://www.drupal.org/node/2300677.
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
199
web/core/modules/jsonapi/tests/src/Functional/BlockTest.php
Normal file
199
web/core/modules/jsonapi/tests/src/Functional/BlockTest.php
Normal file
@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\block\Entity\Block;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "Block" config entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class BlockTest extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['block'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'block';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'block--block';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\block\BlockInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
$this->entity->setVisibilityConfig('user_role', [])->save();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
$block = Block::create([
|
||||
'plugin' => 'llama_block',
|
||||
'region' => 'header',
|
||||
'id' => 'llama',
|
||||
'theme' => 'stark',
|
||||
]);
|
||||
// All blocks can be viewed by the anonymous user by default. An interesting
|
||||
// side effect of this is that any anonymous user is also able to read the
|
||||
// corresponding block config entity via REST, even if an authentication
|
||||
// provider is configured for the block config entity REST resource! In
|
||||
// other words: Block entities do not distinguish between 'view' as in
|
||||
// "render on a page" and 'view' as in "read the configuration".
|
||||
// This prevents that.
|
||||
// @todo Fix this in https://www.drupal.org/node/2820315.
|
||||
$block->setVisibilityConfig('user_role', [
|
||||
'id' => 'user_role',
|
||||
'roles' => ['non-existing-role' => 'non-existing-role'],
|
||||
'negate' => FALSE,
|
||||
'context_mapping' => [
|
||||
'user' => '@user.current_user_context:current_user',
|
||||
],
|
||||
]);
|
||||
$block->save();
|
||||
|
||||
return $block;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/block/block/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'block--block',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'weight' => 0,
|
||||
'langcode' => 'en',
|
||||
'status' => TRUE,
|
||||
'dependencies' => [
|
||||
'theme' => [
|
||||
'stark',
|
||||
],
|
||||
],
|
||||
'theme' => 'stark',
|
||||
'region' => 'header',
|
||||
'provider' => NULL,
|
||||
'plugin' => 'llama_block',
|
||||
'settings' => [
|
||||
'id' => 'broken',
|
||||
'label' => '',
|
||||
'provider' => 'core',
|
||||
'label_display' => 'visible',
|
||||
],
|
||||
'visibility' => [],
|
||||
'drupal_internal__id' => 'llama',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
// @todo Update once https://www.drupal.org/node/2300677 is fixed.
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedCacheContexts(?array $sparse_fieldset = NULL): array {
|
||||
// @see ::createEntity()
|
||||
return array_values(array_diff(parent::getExpectedCacheContexts(), ['user.permissions']));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedCacheTags(?array $sparse_fieldset = NULL): array {
|
||||
// Because the 'user.permissions' cache context is missing, the cache tag
|
||||
// for the anonymous user role is never added automatically.
|
||||
return array_values(array_diff(parent::getExpectedCacheTags(), ['config:user.role.anonymous']));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessMessage($method) {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
return "The block visibility condition 'user_role' denied access.";
|
||||
|
||||
default:
|
||||
return parent::getExpectedUnauthorizedAccessMessage($method);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessCacheability() {
|
||||
// @see \Drupal\block\BlockAccessControlHandler::checkAccess()
|
||||
return parent::getExpectedUnauthorizedAccessCacheability()
|
||||
->setCacheTags([
|
||||
'4xx-response',
|
||||
'config:block.block.llama',
|
||||
'http_response',
|
||||
'user:2',
|
||||
])
|
||||
->setCacheContexts(['url.query_args', 'url.site', 'user.roles']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static function getExpectedCollectionCacheability(AccountInterface $account, array $collection, ?array $sparse_fieldset = NULL, $filtered = FALSE) {
|
||||
return parent::getExpectedCollectionCacheability($account, $collection, $sparse_fieldset, $filtered)
|
||||
->addCacheTags(['user:2'])
|
||||
->addCacheContexts(['user.roles']);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "Comment" content entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class CommentExtrasTest extends CommentTest {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setUp(): void {
|
||||
// Don't run any test methods from CommentTest because those will get run
|
||||
// for CommentTest itself.
|
||||
if (method_exists(parent::class, $this->name())) {
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests POSTing a comment without critical base fields.
|
||||
*
|
||||
* Note that testPostIndividual() is testing with the most minimal
|
||||
* normalization possible: the one returned by ::getNormalizedPostEntity().
|
||||
*
|
||||
* But Comment entities have some very special edge cases:
|
||||
* - base fields that are not marked as required in
|
||||
* \Drupal\comment\Entity\Comment::baseFieldDefinitions() yet in fact are
|
||||
* required.
|
||||
* - base fields that are marked as required, but yet can still result in
|
||||
* validation errors other than "missing required field".
|
||||
*/
|
||||
public function testPostIndividualDxWithoutCriticalBaseFields(): void {
|
||||
$this->setUpAuthorization('POST');
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
|
||||
$url = Url::fromRoute(sprintf('jsonapi.%s.collection.post', static::$resourceTypeName));
|
||||
$request_options = [];
|
||||
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
|
||||
$request_options[RequestOptions::HEADERS]['Content-Type'] = 'application/vnd.api+json';
|
||||
$request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions());
|
||||
|
||||
$remove_field = function (array $normalization, $type, $attribute_name) {
|
||||
unset($normalization['data'][$type][$attribute_name]);
|
||||
return $normalization;
|
||||
};
|
||||
|
||||
// DX: 422 when missing 'entity_type' field.
|
||||
$request_options[RequestOptions::BODY] = Json::encode($remove_field($this->getPostDocument(), 'attributes', 'entity_type'));
|
||||
$response = $this->request('POST', $url, $request_options);
|
||||
$this->assertResourceErrorResponse(422, 'entity_type: This value should not be null.', NULL, $response, '/data/attributes/entity_type');
|
||||
|
||||
// DX: 422 when missing 'entity_id' field.
|
||||
$request_options[RequestOptions::BODY] = Json::encode($remove_field($this->getPostDocument(), 'relationships', 'entity_id'));
|
||||
// @todo Remove the try/catch in https://www.drupal.org/node/2820364.
|
||||
try {
|
||||
$response = $this->request('POST', $url, $request_options);
|
||||
$this->assertResourceErrorResponse(422, 'entity_id: This value should not be null.', NULL, $response, '/data/attributes/entity_id');
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
$this->assertSame("Error: Call to a member function get() on null\nDrupal\\comment\\Plugin\\Validation\\Constraint\\CommentNameConstraintValidator->getAnonymousContactDetailsSetting()() (Line: 96)\n", $e->getMessage());
|
||||
}
|
||||
|
||||
// DX: 422 when missing 'field_name' field.
|
||||
$request_options[RequestOptions::BODY] = Json::encode($remove_field($this->getPostDocument(), 'attributes', 'field_name'));
|
||||
$response = $this->request('POST', $url, $request_options);
|
||||
$this->assertResourceErrorResponse(422, 'field_name: This value should not be null.', NULL, $response, '/data/attributes/field_name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests POSTing a comment with and without 'skip comment approval'.
|
||||
*/
|
||||
public function testPostIndividualSkipCommentApproval(): void {
|
||||
$this->setUpAuthorization('POST');
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
|
||||
// Create request.
|
||||
$request_options = [];
|
||||
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
|
||||
$request_options[RequestOptions::HEADERS]['Content-Type'] = 'application/vnd.api+json';
|
||||
$request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions());
|
||||
$request_options[RequestOptions::BODY] = Json::encode($this->getPostDocument());
|
||||
|
||||
$url = Url::fromRoute('jsonapi.comment--comment.collection.post');
|
||||
|
||||
// Status should be FALSE when posting as anonymous.
|
||||
$response = $this->request('POST', $url, $request_options);
|
||||
$document = $this->getDocumentFromResponse($response);
|
||||
$this->assertResourceResponse(201, FALSE, $response);
|
||||
$this->assertFalse($document['data']['attributes']['status']);
|
||||
$this->assertFalse($this->entityStorage->loadUnchanged(2)->isPublished());
|
||||
|
||||
// Grant anonymous permission to skip comment approval.
|
||||
$this->grantPermissionsToTestedRole(['skip comment approval']);
|
||||
|
||||
// Status must be TRUE when posting as anonymous and skip comment approval.
|
||||
$response = $this->request('POST', $url, $request_options);
|
||||
$document = $this->getDocumentFromResponse($response);
|
||||
$this->assertResourceResponse(201, FALSE, $response);
|
||||
$this->assertTrue($document['data']['attributes']['status']);
|
||||
$this->assertTrue($this->entityStorage->loadUnchanged(3)->isPublished());
|
||||
}
|
||||
|
||||
}
|
||||
398
web/core/modules/jsonapi/tests/src/Functional/CommentTest.php
Normal file
398
web/core/modules/jsonapi/tests/src/Functional/CommentTest.php
Normal file
@ -0,0 +1,398 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\entity_test\EntityTestHelper;
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\comment\Entity\Comment;
|
||||
use Drupal\comment\Entity\CommentType;
|
||||
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
|
||||
use Drupal\comment\Tests\CommentTestTrait;
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\entity_test\Entity\EntityTest;
|
||||
use Drupal\Tests\jsonapi\Traits\CommonCollectionFilterAccessTestPatternsTrait;
|
||||
use Drupal\user\Entity\User;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "Comment" content entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class CommentTest extends ResourceTestBase {
|
||||
|
||||
use CommentTestTrait;
|
||||
use CommonCollectionFilterAccessTestPatternsTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['comment', 'entity_test'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'comment';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'comment--comment';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $patchProtectedFieldNames = [
|
||||
'status' => "The 'administer comments' permission is required.",
|
||||
'name' => "The 'administer comments' permission is required.",
|
||||
'homepage' => "The 'administer comments' permission is required.",
|
||||
'created' => "The 'administer comments' permission is required.",
|
||||
'changed' => NULL,
|
||||
'thread' => NULL,
|
||||
'entity_type' => NULL,
|
||||
'field_name' => NULL,
|
||||
// @todo Uncomment this after https://www.drupal.org/project/drupal/issues/1847608 lands. Until then, it's impossible to test this.
|
||||
// 'pid' => NULL,
|
||||
'uid' => "The 'administer comments' permission is required.",
|
||||
'entity_id' => NULL,
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\comment\CommentInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* @var \Drupal\entity_test\Entity\EntityTest
|
||||
*/
|
||||
private $commentedEntity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
$this->grantPermissionsToTestedRole(['access comments', 'view test entity']);
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
$this->grantPermissionsToTestedRole(['post comments']);
|
||||
break;
|
||||
|
||||
case 'PATCH':
|
||||
$this->grantPermissionsToTestedRole(['edit own comments']);
|
||||
break;
|
||||
|
||||
case 'DELETE':
|
||||
$this->grantPermissionsToTestedRole(['administer comments']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
// Create a "bar" bundle for the "entity_test" entity type and create.
|
||||
$bundle = 'bar';
|
||||
EntityTestHelper::createBundle($bundle, NULL, 'entity_test');
|
||||
|
||||
// Create a comment field on this bundle.
|
||||
$this->addDefaultCommentField('entity_test', 'bar', 'comment');
|
||||
|
||||
// Create a "Camelids" test entity that the comment will be assigned to.
|
||||
$this->commentedEntity = EntityTest::create([
|
||||
'name' => 'Camelids',
|
||||
'type' => 'bar',
|
||||
'comment' => CommentItemInterface::OPEN,
|
||||
]);
|
||||
$this->commentedEntity->save();
|
||||
|
||||
// Create a "Llama" comment.
|
||||
$comment = Comment::create([
|
||||
'comment_body' => [
|
||||
'value' => 'The name "llama" was adopted by European settlers from native Peruvians.',
|
||||
'format' => 'plain_text',
|
||||
],
|
||||
'entity_id' => $this->commentedEntity->id(),
|
||||
'entity_type' => 'entity_test',
|
||||
'field_name' => 'comment',
|
||||
]);
|
||||
$comment->setSubject('Llama')
|
||||
->setOwnerId($this->account->id())
|
||||
->setPublished()
|
||||
->setCreatedTime(123456789)
|
||||
->setChangedTime(123456789);
|
||||
$comment->save();
|
||||
|
||||
return $comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/comment/comment/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
$author = User::load($this->entity->getOwnerId());
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'comment--comment',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'created' => '1973-11-29T21:33:09+00:00',
|
||||
'changed' => (new \DateTime())->setTimestamp($this->entity->getChangedTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
|
||||
'comment_body' => [
|
||||
'value' => 'The name "llama" was adopted by European settlers from native Peruvians.',
|
||||
'format' => 'plain_text',
|
||||
'processed' => "<p>The name "llama" was adopted by European settlers from native Peruvians.</p>\n",
|
||||
],
|
||||
'default_langcode' => TRUE,
|
||||
'entity_type' => 'entity_test',
|
||||
'field_name' => 'comment',
|
||||
'homepage' => NULL,
|
||||
'langcode' => 'en',
|
||||
'name' => NULL,
|
||||
'status' => TRUE,
|
||||
'subject' => 'Llama',
|
||||
'thread' => '01/',
|
||||
'drupal_internal__cid' => (int) $this->entity->id(),
|
||||
],
|
||||
'relationships' => [
|
||||
'uid' => [
|
||||
'data' => [
|
||||
'id' => $author->uuid(),
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => (int) $author->id(),
|
||||
],
|
||||
'type' => 'user--user',
|
||||
],
|
||||
'links' => [
|
||||
'related' => ['href' => $self_url . '/uid'],
|
||||
'self' => ['href' => $self_url . '/relationships/uid'],
|
||||
],
|
||||
],
|
||||
'comment_type' => [
|
||||
'data' => [
|
||||
'id' => CommentType::load('comment')->uuid(),
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => 'comment',
|
||||
],
|
||||
'type' => 'comment_type--comment_type',
|
||||
],
|
||||
'links' => [
|
||||
'related' => ['href' => $self_url . '/comment_type'],
|
||||
'self' => ['href' => $self_url . '/relationships/comment_type'],
|
||||
],
|
||||
],
|
||||
'entity_id' => [
|
||||
'data' => [
|
||||
'id' => $this->commentedEntity->uuid(),
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => (int) $this->commentedEntity->id(),
|
||||
],
|
||||
'type' => 'entity_test--bar',
|
||||
],
|
||||
'links' => [
|
||||
'related' => ['href' => $self_url . '/entity_id'],
|
||||
'self' => ['href' => $self_url . '/relationships/entity_id'],
|
||||
],
|
||||
],
|
||||
'pid' => [
|
||||
'data' => NULL,
|
||||
'links' => [
|
||||
'related' => ['href' => $self_url . '/pid'],
|
||||
'self' => ['href' => $self_url . '/relationships/pid'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
return [
|
||||
'data' => [
|
||||
'type' => 'comment--comment',
|
||||
'attributes' => [
|
||||
'entity_type' => 'entity_test',
|
||||
'field_name' => 'comment',
|
||||
'subject' => 'Drama llama',
|
||||
'comment_body' => [
|
||||
'value' => 'Llamas are awesome.',
|
||||
'format' => 'plain_text',
|
||||
],
|
||||
],
|
||||
'relationships' => [
|
||||
'entity_id' => [
|
||||
'data' => [
|
||||
'type' => 'entity_test--bar',
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => 1,
|
||||
],
|
||||
'id' => EntityTest::load(1)->uuid(),
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedCacheTags(?array $sparse_fieldset = NULL) {
|
||||
$tags = parent::getExpectedCacheTags($sparse_fieldset);
|
||||
if ($sparse_fieldset === NULL || in_array('comment_body', $sparse_fieldset)) {
|
||||
$tags = Cache::mergeTags($tags, ['config:filter.format.plain_text']);
|
||||
}
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedCacheContexts(?array $sparse_fieldset = NULL) {
|
||||
$contexts = parent::getExpectedCacheContexts($sparse_fieldset);
|
||||
if ($sparse_fieldset === NULL || in_array('comment_body', $sparse_fieldset)) {
|
||||
$contexts = Cache::mergeContexts($contexts, ['languages:language_interface', 'theme']);
|
||||
}
|
||||
return $contexts;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessMessage($method) {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
return "The 'access comments' permission is required and the comment must be published.";
|
||||
|
||||
case 'POST':
|
||||
return "The 'post comments' permission is required.";
|
||||
|
||||
case 'PATCH':
|
||||
return "The 'edit own comments' permission is required, the user must be the comment author, and the comment must be published.";
|
||||
|
||||
default:
|
||||
return parent::getExpectedUnauthorizedAccessMessage($method);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessCacheability() {
|
||||
// @see \Drupal\comment\CommentAccessControlHandler::checkAccess()
|
||||
return parent::getExpectedUnauthorizedAccessCacheability()
|
||||
->addCacheTags(['comment:1']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static function entityAccess(EntityInterface $entity, $operation, AccountInterface $account) {
|
||||
// Also reset the 'entity_test' entity access control handler because
|
||||
// comment access also depends on access to the commented entity type.
|
||||
\Drupal::entityTypeManager()->getAccessControlHandler('entity_test')->resetCache();
|
||||
return parent::entityAccess($entity, $operation, $account);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static function getIncludePermissions(): array {
|
||||
return [
|
||||
'type' => ['administer comment types'],
|
||||
'uid' => ['access user profiles'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function testCollectionFilterAccess(): void {
|
||||
// Verify the expected behavior in the common case.
|
||||
$this->doTestCollectionFilterAccessForPublishableEntities('subject', 'access comments', 'administer comments');
|
||||
|
||||
$collection_url = Url::fromRoute('jsonapi.entity_test--bar.collection');
|
||||
$request_options = [];
|
||||
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
|
||||
$request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions());
|
||||
|
||||
// Go back to a simpler scenario: revoke the admin permission, publish the
|
||||
// comment and uninstall the query access test module.
|
||||
$this->revokePermissionsFromTestedRole(['administer comments']);
|
||||
$this->entity->setPublished()->save();
|
||||
$this->assertTrue($this->container->get('module_installer')->uninstall(['jsonapi_test_field_filter_access'], TRUE), 'Uninstalled modules.');
|
||||
// ?filter[spotlight.LABEL]: 1 result. Just as already tested above in
|
||||
// ::doTestCollectionFilterAccessForPublishableEntities().
|
||||
$collection_filter_url = $collection_url->setOption('query', ["filter[spotlight.subject]" => $this->entity->label()]);
|
||||
$response = $this->request('GET', $collection_filter_url, $request_options);
|
||||
$doc = $this->getDocumentFromResponse($response);
|
||||
$this->assertCount(1, $doc['data']);
|
||||
// Mark the commented entity as inaccessible.
|
||||
\Drupal::state()->set('jsonapi__entity_test_filter_access_deny_list', [$this->entity->getCommentedEntityId()]);
|
||||
Cache::invalidateTags(['state:jsonapi__entity_test_filter_access_deny_list']);
|
||||
// ?filter[spotlight.LABEL]: 0 results.
|
||||
$response = $this->request('GET', $collection_filter_url, $request_options);
|
||||
$doc = $this->getDocumentFromResponse($response);
|
||||
$this->assertCount(0, $doc['data']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static function getExpectedCollectionCacheability(AccountInterface $account, array $collection, ?array $sparse_fieldset = NULL, $filtered = FALSE) {
|
||||
$cacheability = parent::getExpectedCollectionCacheability($account, $collection, $sparse_fieldset, $filtered);
|
||||
if ($filtered) {
|
||||
$cacheability->addCacheTags(['state:jsonapi__entity_test_filter_access_deny_list']);
|
||||
}
|
||||
return $cacheability;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function doTestPatchIndividual(): void {
|
||||
// Ensure ::getModifiedEntityForPatchTesting() can pick an alternative value
|
||||
// for the 'entity_id' field.
|
||||
EntityTest::create([
|
||||
'name' => $this->randomString(),
|
||||
'type' => 'bar',
|
||||
])->save();
|
||||
|
||||
parent::doTestPatchIndividual();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\comment\Entity\CommentType;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "CommentType" config entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class CommentTypeTest extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['node', 'comment'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'comment_type';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'comment_type--comment_type';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\comment\CommentTypeInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['administer comment types']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
// Create a "Camelids" comment type.
|
||||
$camelids = CommentType::create([
|
||||
'id' => 'camelids',
|
||||
'label' => 'Camelids',
|
||||
'description' => 'Camelids are large, strictly herbivorous animals with slender necks and long legs.',
|
||||
'target_entity_type_id' => 'node',
|
||||
]);
|
||||
|
||||
$camelids->save();
|
||||
|
||||
return $camelids;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/comment_type/comment_type/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'comment_type--comment_type',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'dependencies' => [],
|
||||
'description' => 'Camelids are large, strictly herbivorous animals with slender necks and long legs.',
|
||||
'label' => 'Camelids',
|
||||
'langcode' => 'en',
|
||||
'status' => TRUE,
|
||||
'target_entity_type_id' => 'node',
|
||||
'drupal_internal__id' => 'camelids',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
// @todo Update in https://www.drupal.org/node/2300677.
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
/**
|
||||
* Resource test base class for config entities.
|
||||
*
|
||||
* @todo Remove this in https://www.drupal.org/node/2300677.
|
||||
*/
|
||||
abstract class ConfigEntityResourceTestBase extends ResourceTestBase {
|
||||
|
||||
/**
|
||||
* A list of test methods to skip.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
const SKIP_METHODS = [
|
||||
'testRelated',
|
||||
'testRelationships',
|
||||
'testPostIndividual',
|
||||
'testPatchIndividual',
|
||||
'testDeleteIndividual',
|
||||
'testRevisions',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setUp(): void {
|
||||
if (in_array($this->name(), static::SKIP_METHODS, TRUE)) {
|
||||
// Skip before installing Drupal to prevent unnecessary use of resources.
|
||||
$this->markTestSkipped("Not yet supported for config entities.");
|
||||
}
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
}
|
||||
125
web/core/modules/jsonapi/tests/src/Functional/ConfigTestTest.php
Normal file
125
web/core/modules/jsonapi/tests/src/Functional/ConfigTestTest.php
Normal file
@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\config_test\Entity\ConfigTest;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "ConfigTest" config entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class ConfigTestTest extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['config_test', 'config_test_rest'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'config_test';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'config_test--config_test';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\config_test\ConfigTestInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['view config_test']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessMessage($method) {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
return "The 'view config_test' permission is required.";
|
||||
|
||||
default:
|
||||
return parent::getExpectedUnauthorizedAccessMessage($method);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
$config_test = ConfigTest::create([
|
||||
'id' => 'llama',
|
||||
'label' => 'Llama',
|
||||
]);
|
||||
$config_test->save();
|
||||
|
||||
return $config_test;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/config_test/config_test/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'config_test--config_test',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'weight' => 0,
|
||||
'langcode' => 'en',
|
||||
'status' => TRUE,
|
||||
'dependencies' => [],
|
||||
'label' => 'Llama',
|
||||
'style' => NULL,
|
||||
'size' => NULL,
|
||||
'size_value' => NULL,
|
||||
'protected_property' => NULL,
|
||||
'drupal_internal__id' => 'llama',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
// @todo Update in https://www.drupal.org/node/2300677.
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "ConfigurableLanguage" config entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class ConfigurableLanguageTest extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['language'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'configurable_language';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'configurable_language--configurable_language';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\Core\Field\Entity\BaseFieldOverride
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['administer languages']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
$configurable_language = ConfigurableLanguage::create([
|
||||
'id' => 'll',
|
||||
'label' => 'Llama Language',
|
||||
]);
|
||||
$configurable_language->save();
|
||||
|
||||
return $configurable_language;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/configurable_language/configurable_language/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'configurable_language--configurable_language',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'dependencies' => [],
|
||||
'direction' => 'ltr',
|
||||
'label' => 'Llama Language',
|
||||
'langcode' => 'en',
|
||||
'locked' => FALSE,
|
||||
'status' => TRUE,
|
||||
'weight' => 0,
|
||||
'drupal_internal__id' => 'll',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
// @todo Update in https://www.drupal.org/node/2300677.
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedCacheContexts(?array $sparse_fieldset = NULL) {
|
||||
return Cache::mergeContexts(parent::getExpectedCacheContexts(), ['languages:language_interface']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests a GET request for a default config entity, which has a _core key.
|
||||
*
|
||||
* @see https://www.drupal.org/project/drupal/issues/2915539
|
||||
*/
|
||||
public function testGetIndividualDefaultConfig(): void {
|
||||
// @todo Remove line below in favor of commented line in https://www.drupal.org/project/drupal/issues/2878463.
|
||||
$url = Url::fromRoute('jsonapi.configurable_language--configurable_language.individual', ['entity' => ConfigurableLanguage::load('en')->uuid()]);
|
||||
/* $url = ConfigurableLanguage::load('en')->toUrl('jsonapi'); */
|
||||
|
||||
$request_options = [];
|
||||
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
|
||||
$request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions());
|
||||
$this->setUpAuthorization('GET');
|
||||
$response = $this->request('GET', $url, $request_options);
|
||||
|
||||
$normalization = $this->getDocumentFromResponse($response);
|
||||
$this->assertArrayNotHasKey('_core', $normalization['data']['attributes']);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\language\Entity\ContentLanguageSettings;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for "ContentLanguageSettings" config entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class ContentLanguageSettingsTest extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['language', 'node'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'language_content_settings';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'language_content_settings--language_content_settings';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\language\ContentLanguageSettingsInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['administer languages']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
// Create a "Camelids" node type.
|
||||
$camelids = NodeType::create([
|
||||
'name' => 'Camelids',
|
||||
'type' => 'camelids',
|
||||
]);
|
||||
$camelids->save();
|
||||
|
||||
$entity = ContentLanguageSettings::create([
|
||||
'target_entity_type_id' => 'node',
|
||||
'target_bundle' => 'camelids',
|
||||
]);
|
||||
$entity->setDefaultLangcode('site_default')
|
||||
->save();
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/language_content_settings/language_content_settings/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'language_content_settings--language_content_settings',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'default_langcode' => 'site_default',
|
||||
'dependencies' => [
|
||||
'config' => [
|
||||
'node.type.camelids',
|
||||
],
|
||||
],
|
||||
'langcode' => 'en',
|
||||
'language_alterable' => FALSE,
|
||||
'status' => TRUE,
|
||||
'target_bundle' => 'camelids',
|
||||
'target_entity_type_id' => 'node',
|
||||
'drupal_internal__id' => 'node.camelids',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
// @todo Update in https://www.drupal.org/node/2300677.
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedCacheContexts(?array $sparse_fieldset = NULL) {
|
||||
return Cache::mergeContexts(parent::getExpectedCacheContexts(), ['languages:language_interface']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createAnotherEntity($key) {
|
||||
NodeType::create([
|
||||
'name' => 'Llamaids',
|
||||
'type' => 'llamaids',
|
||||
])->save();
|
||||
|
||||
$entity = ContentLanguageSettings::create([
|
||||
'target_entity_type_id' => 'node',
|
||||
'target_bundle' => 'llamaids',
|
||||
]);
|
||||
$entity->setDefaultLangcode('site_default');
|
||||
$entity->save();
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static function getExpectedCollectionCacheability(AccountInterface $account, array $collection, ?array $sparse_fieldset = NULL, $filtered = FALSE) {
|
||||
$cacheability = parent::getExpectedCollectionCacheability($account, $collection, $sparse_fieldset, $filtered);
|
||||
if (static::entityAccess(reset($collection), 'view', $account)->isAllowed()) {
|
||||
$cacheability->addCacheContexts(['languages:language_interface']);
|
||||
}
|
||||
return $cacheability;
|
||||
}
|
||||
|
||||
}
|
||||
117
web/core/modules/jsonapi/tests/src/Functional/DateFormatTest.php
Normal file
117
web/core/modules/jsonapi/tests/src/Functional/DateFormatTest.php
Normal file
@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Core\Datetime\Entity\DateFormat;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "DateFormat" config entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class DateFormatTest extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'date_format';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'date_format--date_format';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $anonymousUsersCanViewLabels = TRUE;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\Core\Datetime\DateFormatInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['administer site configuration']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
// Create a date format.
|
||||
$date_format = DateFormat::create([
|
||||
'id' => 'llama',
|
||||
'label' => 'Llama',
|
||||
'pattern' => 'F d, Y',
|
||||
]);
|
||||
|
||||
$date_format->save();
|
||||
|
||||
return $date_format;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/date_format/date_format/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'date_format--date_format',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'dependencies' => [],
|
||||
'label' => 'Llama',
|
||||
'langcode' => 'en',
|
||||
'locked' => FALSE,
|
||||
'pattern' => 'F d, Y',
|
||||
'status' => TRUE,
|
||||
'drupal_internal__id' => 'llama',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
// @todo Update in https://www.drupal.org/node/2300677.
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
225
web/core/modules/jsonapi/tests/src/Functional/EditorTest.php
Normal file
225
web/core/modules/jsonapi/tests/src/Functional/EditorTest.php
Normal file
@ -0,0 +1,225 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\ckeditor5\Plugin\CKEditor5Plugin\Heading;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "Editor" config entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class EditorTest extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['filter', 'editor', 'ckeditor5'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'editor';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'editor--editor';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\editor\EditorInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['administer filters']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
// Create a "Llama" filter format.
|
||||
$llama_format = FilterFormat::create([
|
||||
'name' => 'Llama',
|
||||
'format' => 'llama',
|
||||
'langcode' => 'es',
|
||||
'filters' => [
|
||||
'filter_html' => [
|
||||
'status' => TRUE,
|
||||
'settings' => [
|
||||
'allowed_html' => '<p> <a> <b> <lo>',
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$llama_format->save();
|
||||
|
||||
// Create a "Camelids" editor.
|
||||
$camelids = Editor::create([
|
||||
'format' => 'llama',
|
||||
'editor' => 'ckeditor5',
|
||||
'image_upload' => [
|
||||
'status' => FALSE,
|
||||
],
|
||||
]);
|
||||
$camelids
|
||||
->setImageUploadSettings([
|
||||
'status' => TRUE,
|
||||
'scheme' => 'public',
|
||||
'directory' => 'inline-images',
|
||||
'max_size' => NULL,
|
||||
'max_dimensions' => [
|
||||
'width' => NULL,
|
||||
'height' => NULL,
|
||||
],
|
||||
])
|
||||
->save();
|
||||
|
||||
return $camelids;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/editor/editor/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'editor--editor',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'dependencies' => [
|
||||
'config' => [
|
||||
'filter.format.llama',
|
||||
],
|
||||
'module' => [
|
||||
'ckeditor5',
|
||||
],
|
||||
],
|
||||
'editor' => 'ckeditor5',
|
||||
'image_upload' => [
|
||||
'status' => TRUE,
|
||||
'scheme' => 'public',
|
||||
'directory' => 'inline-images',
|
||||
'max_size' => NULL,
|
||||
'max_dimensions' => [
|
||||
'width' => NULL,
|
||||
'height' => NULL,
|
||||
],
|
||||
],
|
||||
'langcode' => 'en',
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'items' => ['heading', 'bold', 'italic'],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_heading' => Heading::DEFAULT_CONFIGURATION,
|
||||
],
|
||||
],
|
||||
'status' => TRUE,
|
||||
'drupal_internal__format' => 'llama',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
// @todo Update in https://www.drupal.org/node/2300677.
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessMessage($method): string {
|
||||
return "The 'administer filters' permission is required.";
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createAnotherEntity($key) {
|
||||
FilterFormat::create([
|
||||
'name' => 'Pachyderm',
|
||||
'format' => 'pachyderm',
|
||||
'langcode' => 'fr',
|
||||
'filters' => [
|
||||
'filter_html' => [
|
||||
'status' => TRUE,
|
||||
'settings' => [
|
||||
'allowed_html' => '<p> <a> <b> <lo>',
|
||||
],
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
|
||||
$entity = Editor::create([
|
||||
'format' => 'pachyderm',
|
||||
'editor' => 'ckeditor5',
|
||||
'image_upload' => [
|
||||
'status' => FALSE,
|
||||
],
|
||||
]);
|
||||
|
||||
$entity->setImageUploadSettings([
|
||||
'status' => TRUE,
|
||||
'scheme' => 'public',
|
||||
'directory' => 'inline-images',
|
||||
'max_size' => NULL,
|
||||
'max_dimensions' => [
|
||||
'width' => NULL,
|
||||
'height' => NULL,
|
||||
],
|
||||
])->save();
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static function entityAccess(EntityInterface $entity, $operation, AccountInterface $account) {
|
||||
// Also reset the 'filter_format' entity access control handler because
|
||||
// editor access also depends on access to the configured filter format.
|
||||
\Drupal::entityTypeManager()->getAccessControlHandler('filter_format')->resetCache();
|
||||
return parent::entityAccess($entity, $operation, $account);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Core\Entity\Entity\EntityFormDisplay;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "EntityFormDisplay" config entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class EntityFormDisplayTest extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['node', 'field_ui'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'entity_form_display';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'entity_form_display--entity_form_display';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['administer node form display']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
// Create a "Camelids" node type.
|
||||
$camelids = NodeType::create([
|
||||
'name' => 'Camelids',
|
||||
'type' => 'camelids',
|
||||
]);
|
||||
$camelids->save();
|
||||
|
||||
// Create a form display.
|
||||
$form_display = EntityFormDisplay::create([
|
||||
'targetEntityType' => 'node',
|
||||
'bundle' => 'camelids',
|
||||
'mode' => 'default',
|
||||
]);
|
||||
$form_display->save();
|
||||
|
||||
return $form_display;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/entity_form_display/entity_form_display/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'entity_form_display--entity_form_display',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'bundle' => 'camelids',
|
||||
'content' => [
|
||||
'created' => [
|
||||
'type' => 'datetime_timestamp',
|
||||
'weight' => 10,
|
||||
'region' => 'content',
|
||||
'settings' => [],
|
||||
'third_party_settings' => [],
|
||||
],
|
||||
'promote' => [
|
||||
'type' => 'boolean_checkbox',
|
||||
'settings' => [
|
||||
'display_label' => TRUE,
|
||||
],
|
||||
'weight' => 15,
|
||||
'region' => 'content',
|
||||
'third_party_settings' => [],
|
||||
],
|
||||
'status' => [
|
||||
'type' => 'boolean_checkbox',
|
||||
'weight' => 120,
|
||||
'region' => 'content',
|
||||
'settings' => [
|
||||
'display_label' => TRUE,
|
||||
],
|
||||
'third_party_settings' => [],
|
||||
],
|
||||
'sticky' => [
|
||||
'type' => 'boolean_checkbox',
|
||||
'settings' => [
|
||||
'display_label' => TRUE,
|
||||
],
|
||||
'weight' => 16,
|
||||
'region' => 'content',
|
||||
'third_party_settings' => [],
|
||||
],
|
||||
'title' => [
|
||||
'type' => 'string_textfield',
|
||||
'weight' => -5,
|
||||
'region' => 'content',
|
||||
'settings' => [
|
||||
'size' => 60,
|
||||
'placeholder' => '',
|
||||
],
|
||||
'third_party_settings' => [],
|
||||
],
|
||||
'uid' => [
|
||||
'type' => 'entity_reference_autocomplete',
|
||||
'weight' => 5,
|
||||
'settings' => [
|
||||
'match_operator' => 'CONTAINS',
|
||||
'match_limit' => 10,
|
||||
'size' => 60,
|
||||
'placeholder' => '',
|
||||
],
|
||||
'region' => 'content',
|
||||
'third_party_settings' => [],
|
||||
],
|
||||
],
|
||||
'dependencies' => [
|
||||
'config' => [
|
||||
'node.type.camelids',
|
||||
],
|
||||
],
|
||||
'hidden' => [],
|
||||
'langcode' => 'en',
|
||||
'mode' => 'default',
|
||||
'status' => NULL,
|
||||
'targetEntityType' => 'node',
|
||||
'drupal_internal__id' => 'node.camelids.default',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
// @todo Update in https://www.drupal.org/node/2300677.
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessMessage($method): string {
|
||||
return "The 'administer node form display' permission is required.";
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createAnotherEntity($key) {
|
||||
NodeType::create([
|
||||
'name' => 'Llamaids',
|
||||
'type' => 'llamaids',
|
||||
])->save();
|
||||
|
||||
$entity = EntityFormDisplay::create([
|
||||
'targetEntityType' => 'node',
|
||||
'bundle' => 'llamaids',
|
||||
'mode' => 'default',
|
||||
]);
|
||||
$entity->save();
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Core\Entity\Entity\EntityFormMode;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "EntityFormMode" config entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class EntityFormModeTest extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @todo Remove 'field_ui' when https://www.drupal.org/node/2867266.
|
||||
*/
|
||||
protected static $modules = ['user', 'field_ui'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'entity_form_mode';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'entity_form_mode--entity_form_mode';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityFormModeInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['administer display modes']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
$entity_form_mode = EntityFormMode::create([
|
||||
'id' => 'user.test',
|
||||
'label' => 'Test',
|
||||
'description' => NULL,
|
||||
'targetEntityType' => 'user',
|
||||
]);
|
||||
$entity_form_mode->save();
|
||||
return $entity_form_mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/entity_form_mode/entity_form_mode/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'entity_form_mode--entity_form_mode',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'cache' => TRUE,
|
||||
'dependencies' => [
|
||||
'module' => [
|
||||
'user',
|
||||
],
|
||||
],
|
||||
'description' => '',
|
||||
'label' => 'Test',
|
||||
'langcode' => 'en',
|
||||
'status' => TRUE,
|
||||
'targetEntityType' => 'user',
|
||||
'drupal_internal__id' => 'user.test',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
// @todo Update in https://www.drupal.org/node/2300677.
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,197 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\entity_test\Entity\EntityTestComputedField;
|
||||
use Drupal\user\Entity\User;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "EntityTestComputedField" content entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class EntityTestComputedFieldTest extends ResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['entity_test'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'entity_test_computed_field';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'entity_test_computed_field--entity_test_computed_field';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $patchProtectedFieldNames = [];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\entity_test\Entity\EntityTestComputedField
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['administer entity_test content']);
|
||||
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
$this->grantPermissionsToTestedRole(['view test entity']);
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
$this->grantPermissionsToTestedRole(['create entity_test entity_test_with_bundle entities']);
|
||||
break;
|
||||
|
||||
case 'PATCH':
|
||||
case 'DELETE':
|
||||
$this->grantPermissionsToTestedRole(['administer entity_test content']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
$entity_test = EntityTestComputedField::create([
|
||||
'name' => 'Llama',
|
||||
'type' => 'entity_test_computed_field',
|
||||
]);
|
||||
|
||||
$entity_test->setOwnerId(0);
|
||||
$entity_test->save();
|
||||
|
||||
return $entity_test;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/entity_test_computed_field/entity_test_computed_field/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
$author = User::load(0);
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'entity_test_computed_field--entity_test_computed_field',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'created' => (new \DateTime())->setTimestamp((int) $this->entity->get('created')->value)->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
|
||||
'name' => 'Llama',
|
||||
'drupal_internal__id' => 1,
|
||||
'computed_string_field' => NULL,
|
||||
'computed_test_cacheable_string_field' => 'computed test cacheable string field',
|
||||
'computed_test_cacheable_integer_field' => 0,
|
||||
],
|
||||
'relationships' => [
|
||||
'computed_reference_field' => [
|
||||
'data' => NULL,
|
||||
'links' => [
|
||||
'related' => ['href' => $self_url . '/computed_reference_field'],
|
||||
'self' => ['href' => $self_url . '/relationships/computed_reference_field'],
|
||||
],
|
||||
],
|
||||
'user_id' => [
|
||||
'data' => [
|
||||
'id' => $author->uuid(),
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => (int) $author->id(),
|
||||
],
|
||||
'type' => 'user--user',
|
||||
],
|
||||
'links' => [
|
||||
'related' => ['href' => $self_url . '/user_id'],
|
||||
'self' => ['href' => $self_url . '/relationships/user_id'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
return [
|
||||
'data' => [
|
||||
'type' => 'entity_test_computed_field--entity_test_computed_field',
|
||||
'attributes' => [
|
||||
'name' => 'Drama llama',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getSparseFieldSets(): array {
|
||||
// EntityTest's owner field name is `user_id`, not `uid`, which breaks
|
||||
// nested sparse fieldset tests.
|
||||
return array_diff_key(parent::getSparseFieldSets(), array_flip([
|
||||
'nested_empty_fieldset',
|
||||
'nested_fieldset_with_owner_fieldset',
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the expected cache contexts for the response.
|
||||
*/
|
||||
protected function getExpectedCacheContexts(?array $sparse_fieldset = NULL) {
|
||||
$cache_contexts = parent::getExpectedCacheContexts($sparse_fieldset);
|
||||
if ($sparse_fieldset === NULL || in_array('computed_test_cacheable_string_field', $sparse_fieldset)) {
|
||||
$cache_contexts = Cache::mergeContexts($cache_contexts, ['url.query_args']);
|
||||
}
|
||||
|
||||
return $cache_contexts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the expected cache tags for the response.
|
||||
*/
|
||||
protected function getExpectedCacheTags(?array $sparse_fieldset = NULL) {
|
||||
$expected_cache_tags = parent::getExpectedCacheTags($sparse_fieldset);
|
||||
if ($sparse_fieldset === NULL || in_array('computed_test_cacheable_string_field', $sparse_fieldset)) {
|
||||
$expected_cache_tags = Cache::mergeTags($expected_cache_tags, ['field:computed_test_cacheable_string_field']);
|
||||
}
|
||||
|
||||
return $expected_cache_tags;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\entity_test\Entity\EntityTestMapField;
|
||||
use Drupal\user\Entity\User;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "EntityTestMapField" content entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class EntityTestMapFieldTest extends ResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['entity_test'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'entity_test_map_field';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'entity_test_map_field--entity_test_map_field';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $patchProtectedFieldNames = [];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\entity_test\Entity\EntityTestMapField
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* The complex nested value to assign to a @FieldType=map field.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $mapValue = [
|
||||
'key1' => 'value',
|
||||
'key2' => 'no, val you',
|
||||
'π' => 3.14159,
|
||||
TRUE => 42,
|
||||
'nested' => [
|
||||
'bird' => 'robin',
|
||||
'doll' => 'Russian',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['administer entity_test content']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
$entity = EntityTestMapField::create([
|
||||
'name' => 'Llama',
|
||||
'type' => 'entity_test_map_field',
|
||||
'data' => [
|
||||
static::$mapValue,
|
||||
],
|
||||
]);
|
||||
$entity->setOwnerId(0);
|
||||
$entity->save();
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/entity_test_map_field/entity_test_map_field/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
$author = User::load(0);
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'entity_test_map_field--entity_test_map_field',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'created' => (new \DateTime())->setTimestamp((int) $this->entity->get('created')->value)->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
|
||||
'langcode' => 'en',
|
||||
'name' => 'Llama',
|
||||
'data' => static::$mapValue,
|
||||
'drupal_internal__id' => 1,
|
||||
],
|
||||
'relationships' => [
|
||||
'user_id' => [
|
||||
'data' => [
|
||||
'id' => $author->uuid(),
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => (int) $author->id(),
|
||||
],
|
||||
'type' => 'user--user',
|
||||
],
|
||||
'links' => [
|
||||
'related' => ['href' => $self_url . '/user_id'],
|
||||
'self' => ['href' => $self_url . '/relationships/user_id'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
return [
|
||||
'data' => [
|
||||
'type' => 'entity_test_map_field--entity_test_map_field',
|
||||
'attributes' => [
|
||||
'name' => 'Drama llama',
|
||||
'data' => static::$mapValue,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessMessage($method): string {
|
||||
return "The 'administer entity_test content' permission is required.";
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getSparseFieldSets(): array {
|
||||
// EntityTestMapField's owner field name is `user_id`, not `uid`, which
|
||||
// breaks nested sparse fieldset tests.
|
||||
return array_diff_key(parent::getSparseFieldSets(), array_flip([
|
||||
'nested_empty_fieldset',
|
||||
'nested_fieldset_with_owner_fieldset',
|
||||
]));
|
||||
}
|
||||
|
||||
}
|
||||
205
web/core/modules/jsonapi/tests/src/Functional/EntityTestTest.php
Normal file
205
web/core/modules/jsonapi/tests/src/Functional/EntityTestTest.php
Normal file
@ -0,0 +1,205 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Core\Field\BaseFieldDefinition;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\entity_test\Entity\EntityTest;
|
||||
use Drupal\user\Entity\User;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "EntityTest" content entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class EntityTestTest extends ResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['entity_test'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'entity_test';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'entity_test--entity_test';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $patchProtectedFieldNames = [];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\entity_test\Entity\EntityTest
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
$this->grantPermissionsToTestedRole(['view test entity']);
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
$this->grantPermissionsToTestedRole(['create entity_test entity_test_with_bundle entities']);
|
||||
break;
|
||||
|
||||
case 'PATCH':
|
||||
case 'DELETE':
|
||||
$this->grantPermissionsToTestedRole(['administer entity_test content']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
// Set flag so that internal field 'internal_string_field' is created.
|
||||
// @see entity_test_entity_base_field_info()
|
||||
$this->container->get('state')->set('entity_test.internal_field', TRUE);
|
||||
$field_storage_definition = BaseFieldDefinition::create('string')
|
||||
->setLabel('Internal field')
|
||||
->setInternal(TRUE);
|
||||
\Drupal::entityDefinitionUpdateManager()->installFieldStorageDefinition('internal_string_field', 'entity_test', 'entity_test', $field_storage_definition);
|
||||
|
||||
$entity_test = EntityTest::create([
|
||||
'name' => 'Llama',
|
||||
'type' => 'entity_test',
|
||||
// Set a value for the internal field to confirm that it will not be
|
||||
// returned in normalization.
|
||||
// @see entity_test_entity_base_field_info().
|
||||
'internal_string_field' => [
|
||||
'value' => 'This value shall not be internal!',
|
||||
],
|
||||
]);
|
||||
$entity_test->setOwnerId(0);
|
||||
$entity_test->save();
|
||||
|
||||
return $entity_test;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/entity_test/entity_test/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
$author = User::load(0);
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'entity_test--entity_test',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'created' => (new \DateTime())->setTimestamp((int) $this->entity->get('created')->value)->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
|
||||
'field_test_text' => NULL,
|
||||
'langcode' => 'en',
|
||||
'name' => 'Llama',
|
||||
'entity_test_type' => 'entity_test',
|
||||
'drupal_internal__id' => 1,
|
||||
],
|
||||
'relationships' => [
|
||||
'user_id' => [
|
||||
'data' => [
|
||||
'id' => $author->uuid(),
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => (int) $author->id(),
|
||||
],
|
||||
'type' => 'user--user',
|
||||
],
|
||||
'links' => [
|
||||
'related' => ['href' => $self_url . '/user_id'],
|
||||
'self' => ['href' => $self_url . '/relationships/user_id'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
return [
|
||||
'data' => [
|
||||
'type' => 'entity_test--entity_test',
|
||||
'attributes' => [
|
||||
'name' => 'Drama llama',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessMessage($method) {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
return "The 'view test entity' permission is required.";
|
||||
|
||||
case 'POST':
|
||||
return "The following permissions are required: 'administer entity_test content' OR 'administer entity_test_with_bundle content' OR 'create entity_test entity_test_with_bundle entities'.";
|
||||
|
||||
default:
|
||||
return parent::getExpectedUnauthorizedAccessMessage($method);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getSparseFieldSets(): array {
|
||||
// EntityTest's owner field name is `user_id`, not `uid`, which breaks
|
||||
// nested sparse fieldset tests.
|
||||
return array_diff_key(parent::getSparseFieldSets(), array_flip([
|
||||
'nested_empty_fieldset',
|
||||
'nested_fieldset_with_owner_fieldset',
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static function getExpectedCollectionCacheability(AccountInterface $account, array $collection, ?array $sparse_fieldset = NULL, $filtered = FALSE) {
|
||||
$cacheability = parent::getExpectedCollectionCacheability($account, $collection, $sparse_fieldset, $filtered);
|
||||
if ($filtered) {
|
||||
$cacheability->addCacheTags(['state:jsonapi__entity_test_filter_access_deny_list']);
|
||||
}
|
||||
return $cacheability;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Core\Entity\Entity\EntityViewDisplay;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "EntityViewDisplay" config entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class EntityViewDisplayTest extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['node', 'field_ui'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'entity_view_display';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'entity_view_display--entity_view_display';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['administer node display']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
// Create a "Camelids" node type.
|
||||
$camelids = NodeType::create([
|
||||
'name' => 'Camelids',
|
||||
'type' => 'camelids',
|
||||
]);
|
||||
$camelids->save();
|
||||
|
||||
// Create a view display.
|
||||
$view_display = EntityViewDisplay::create([
|
||||
'targetEntityType' => 'node',
|
||||
'bundle' => 'camelids',
|
||||
'mode' => 'default',
|
||||
'status' => TRUE,
|
||||
]);
|
||||
$view_display->save();
|
||||
|
||||
return $view_display;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/entity_view_display/entity_view_display/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'entity_view_display--entity_view_display',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'bundle' => 'camelids',
|
||||
'content' => [
|
||||
'links' => [
|
||||
'region' => 'content',
|
||||
'weight' => 100,
|
||||
'settings' => [],
|
||||
'third_party_settings' => [],
|
||||
],
|
||||
],
|
||||
'dependencies' => [
|
||||
'config' => [
|
||||
'node.type.camelids',
|
||||
],
|
||||
'module' => [
|
||||
'user',
|
||||
],
|
||||
],
|
||||
'hidden' => [],
|
||||
'langcode' => 'en',
|
||||
'mode' => 'default',
|
||||
'status' => TRUE,
|
||||
'targetEntityType' => 'node',
|
||||
'drupal_internal__id' => 'node.camelids.default',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
// @todo Update in https://www.drupal.org/node/2300677.
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessMessage($method): string {
|
||||
return "The 'administer node display' permission is required.";
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createAnotherEntity($key) {
|
||||
NodeType::create([
|
||||
'name' => 'Pachyderms',
|
||||
'type' => 'pachyderms',
|
||||
])->save();
|
||||
|
||||
$entity = EntityViewDisplay::create([
|
||||
'targetEntityType' => 'node',
|
||||
'bundle' => 'pachyderms',
|
||||
'mode' => 'default',
|
||||
'status' => TRUE,
|
||||
]);
|
||||
$entity->save();
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Core\Entity\Entity\EntityViewMode;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "EntityViewMode" config entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class EntityViewModeTest extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @todo Remove 'field_ui' when https://www.drupal.org/node/2867266.
|
||||
*/
|
||||
protected static $modules = ['user', 'field_ui'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'entity_view_mode';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'entity_view_mode--entity_view_mode';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityViewModeInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['administer display modes']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
$entity_view_mode = EntityViewMode::create([
|
||||
'id' => 'user.test',
|
||||
'label' => 'Test',
|
||||
'description' => '',
|
||||
'targetEntityType' => 'user',
|
||||
]);
|
||||
$entity_view_mode->save();
|
||||
return $entity_view_mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/entity_view_mode/entity_view_mode/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'entity_view_mode--entity_view_mode',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'cache' => TRUE,
|
||||
'dependencies' => [
|
||||
'module' => [
|
||||
'user',
|
||||
],
|
||||
],
|
||||
'label' => 'Test',
|
||||
'langcode' => 'en',
|
||||
'description' => '',
|
||||
'status' => TRUE,
|
||||
'targetEntityType' => 'user',
|
||||
'drupal_internal__id' => 'user.test',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
// @todo Update in https://www.drupal.org/node/2300677.
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\Tests\jsonapi\Traits\GetDocumentFromResponseTrait;
|
||||
use Drupal\Tests\user\Traits\UserCreationTrait;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
|
||||
/**
|
||||
* Makes assertions about the JSON:API behavior for internal entities.
|
||||
*
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class EntryPointTest extends BrowserTestBase {
|
||||
|
||||
use GetDocumentFromResponseTrait;
|
||||
use JsonApiRequestTestTrait;
|
||||
use UserCreationTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'node',
|
||||
'jsonapi',
|
||||
'basic_auth',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* Test GET to the entry point.
|
||||
*/
|
||||
public function testEntryPoint(): void {
|
||||
$request_options = [];
|
||||
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
|
||||
$response = $this->request('GET', Url::fromUri('base://jsonapi'), $request_options);
|
||||
$document = $this->getDocumentFromResponse($response);
|
||||
$expected_cache_contexts = [
|
||||
'url.query_args',
|
||||
'url.site',
|
||||
'user.roles:authenticated',
|
||||
];
|
||||
$this->assertTrue($response->hasHeader('X-Drupal-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]));
|
||||
$links = $document['links'];
|
||||
$this->assertMatchesRegularExpression('/.*\/jsonapi/', $links['self']['href']);
|
||||
$this->assertMatchesRegularExpression('/.*\/jsonapi\/user\/user/', $links['user--user']['href']);
|
||||
$this->assertMatchesRegularExpression('/.*\/jsonapi\/node_type\/node_type/', $links['node_type--node_type']['href']);
|
||||
$this->assertArrayNotHasKey('meta', $document);
|
||||
|
||||
// A `me` link must be present for authenticated users.
|
||||
$user = $this->createUser();
|
||||
$request_options[RequestOptions::HEADERS]['Authorization'] = 'Basic ' . base64_encode($user->name->value . ':' . $user->passRaw);
|
||||
$response = $this->request('GET', Url::fromUri('base://jsonapi'), $request_options);
|
||||
$document = $this->getDocumentFromResponse($response);
|
||||
$this->assertArrayHasKey('meta', $document);
|
||||
$this->assertStringEndsWith('/jsonapi/user/user/' . $user->uuid(), $document['meta']['links']['me']['href']);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\entity_test\Entity\EntityTest;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\Tests\jsonapi\Traits\GetDocumentFromResponseTrait;
|
||||
use Drupal\user\Entity\Role;
|
||||
use Drupal\user\RoleInterface;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
|
||||
/**
|
||||
* Asserts external normalizers are handled as expected by the JSON:API module.
|
||||
*
|
||||
* @see jsonapi.normalizers
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class ExternalNormalizersTest extends BrowserTestBase {
|
||||
|
||||
use GetDocumentFromResponseTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* The original value for the test field.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const VALUE_ORIGINAL = 'Llamas are super awesome!';
|
||||
|
||||
/**
|
||||
* The expected overridden value for the test field.
|
||||
*
|
||||
* @see \Drupal\jsonapi_test_field_type\Normalizer\StringNormalizer
|
||||
* @see \Drupal\jsonapi_test_data_type\Normalizer\StringNormalizer
|
||||
*/
|
||||
const VALUE_OVERRIDDEN = 'Llamas are NOT awesome!';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'jsonapi',
|
||||
'entity_test',
|
||||
];
|
||||
|
||||
/**
|
||||
* The test entity.
|
||||
*
|
||||
* @var \Drupal\entity_test\Entity\EntityTest
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
// This test is not about access control at all, so allow anonymous users to
|
||||
// view and create the test entities.
|
||||
Role::load(RoleInterface::ANONYMOUS_ID)
|
||||
->grantPermission('view test entity')
|
||||
->grantPermission('create entity_test entity_test_with_bundle entities')
|
||||
->save();
|
||||
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
|
||||
FieldStorageConfig::create([
|
||||
'field_name' => 'field_test',
|
||||
'type' => 'string',
|
||||
'entity_type' => 'entity_test',
|
||||
])
|
||||
->save();
|
||||
FieldConfig::create([
|
||||
'field_name' => 'field_test',
|
||||
'entity_type' => 'entity_test',
|
||||
'bundle' => 'entity_test',
|
||||
])
|
||||
->save();
|
||||
|
||||
$this->entity = EntityTest::create([
|
||||
'name' => 'Llama',
|
||||
'type' => 'entity_test',
|
||||
'field_test' => static::VALUE_ORIGINAL,
|
||||
]);
|
||||
$this->entity->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests a format-agnostic normalizer.
|
||||
*
|
||||
* @param string $test_module
|
||||
* The test module to install, which comes with a high-priority normalizer.
|
||||
* @param string $expected_value_jsonapi_normalization
|
||||
* The expected JSON:API normalization of the tested field. Must be either
|
||||
* - static::VALUE_ORIGINAL (normalizer IS NOT expected to override)
|
||||
* - static::VALUE_OVERRIDDEN (normalizer IS expected to override)
|
||||
* @param string $expected_value_jsonapi_denormalization
|
||||
* The expected JSON:API denormalization of the tested field. Must be either
|
||||
* - static::VALUE_OVERRIDDEN (denormalizer IS NOT expected to override)
|
||||
* - static::VALUE_ORIGINAL (denormalizer IS expected to override)
|
||||
*
|
||||
* @dataProvider providerTestFormatAgnosticNormalizers
|
||||
*/
|
||||
public function testFormatAgnosticNormalizers($test_module, $expected_value_jsonapi_normalization, $expected_value_jsonapi_denormalization): void {
|
||||
assert(in_array($expected_value_jsonapi_normalization, [static::VALUE_ORIGINAL, static::VALUE_OVERRIDDEN], TRUE));
|
||||
assert(in_array($expected_value_jsonapi_denormalization, [static::VALUE_ORIGINAL, static::VALUE_OVERRIDDEN], TRUE));
|
||||
|
||||
// Asserts the entity contains the value we set.
|
||||
$this->assertSame(static::VALUE_ORIGINAL, $this->entity->field_test->value);
|
||||
|
||||
// Asserts normalizing the entity using core's 'serializer' service DOES
|
||||
// yield the value we set.
|
||||
$core_normalization = $this->container->get('serializer')->normalize($this->entity);
|
||||
$this->assertSame(static::VALUE_ORIGINAL, $core_normalization['field_test'][0]['value']);
|
||||
|
||||
// Asserts denormalizing the entity using core's 'serializer' service DOES
|
||||
// yield the value we set.
|
||||
$core_normalization['field_test'][0]['value'] = static::VALUE_OVERRIDDEN;
|
||||
$denormalized_entity = $this->container->get('serializer')->denormalize($core_normalization, EntityTest::class, 'json', []);
|
||||
$this->assertInstanceOf(EntityTest::class, $denormalized_entity);
|
||||
$this->assertSame(static::VALUE_OVERRIDDEN, $denormalized_entity->field_test->value);
|
||||
|
||||
// Install test module that contains a high-priority alternative normalizer.
|
||||
$this->container->get('module_installer')->install([$test_module]);
|
||||
$this->rebuildContainer();
|
||||
|
||||
// Asserts normalizing the entity using core's 'serializer' service DOES NOT
|
||||
// ANYMORE yield the value we set.
|
||||
$core_normalization = $this->container->get('serializer')->normalize($this->entity);
|
||||
$this->assertSame(static::VALUE_OVERRIDDEN, $core_normalization['field_test'][0]['value']);
|
||||
|
||||
// Asserts denormalizing the entity using core's 'serializer' service DOES
|
||||
// NOT ANYMORE yield the value we set.
|
||||
$core_normalization = $this->container->get('serializer')->normalize($this->entity);
|
||||
$core_normalization['field_test'][0]['value'] = static::VALUE_OVERRIDDEN;
|
||||
$denormalized_entity = $this->container->get('serializer')->denormalize($core_normalization, EntityTest::class, 'json', []);
|
||||
$this->assertInstanceOf(EntityTest::class, $denormalized_entity);
|
||||
$this->assertSame(static::VALUE_ORIGINAL, $denormalized_entity->field_test->value);
|
||||
|
||||
// Asserts the expected JSON:API normalization.
|
||||
// @todo Remove line below in favor of commented line in https://www.drupal.org/project/drupal/issues/2878463.
|
||||
$url = Url::fromRoute('jsonapi.entity_test--entity_test.individual', ['entity' => $this->entity->uuid()]);
|
||||
// $url = $this->entity->toUrl('jsonapi');
|
||||
$client = $this->getSession()->getDriver()->getClient()->getClient();
|
||||
$response = $client->request('GET', $url->setAbsolute(TRUE)->toString());
|
||||
$document = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame($expected_value_jsonapi_normalization, $document['data']['attributes']['field_test']);
|
||||
|
||||
// Asserts the expected JSON:API denormalization.
|
||||
$request_options = [];
|
||||
$request_options[RequestOptions::BODY] = Json::encode([
|
||||
'data' => [
|
||||
'type' => 'entity_test--entity_test',
|
||||
'attributes' => [
|
||||
'field_test' => static::VALUE_OVERRIDDEN,
|
||||
],
|
||||
],
|
||||
]);
|
||||
$request_options[RequestOptions::HEADERS]['Content-Type'] = 'application/vnd.api+json';
|
||||
$response = $client->request('POST', Url::fromRoute('jsonapi.entity_test--entity_test.collection.post')->setAbsolute(TRUE)->toString(), $request_options);
|
||||
$document = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame(static::VALUE_OVERRIDDEN, $document['data']['attributes']['field_test']);
|
||||
$entity_type_manager = $this->container->get('entity_type.manager');
|
||||
$uuid_key = $entity_type_manager->getDefinition('entity_test')->getKey('uuid');
|
||||
$entities = $entity_type_manager
|
||||
->getStorage('entity_test')
|
||||
->loadByProperties([$uuid_key => $document['data']['id']]);
|
||||
$created_entity = reset($entities);
|
||||
$this->assertSame($expected_value_jsonapi_denormalization, $created_entity->field_test->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider.
|
||||
*
|
||||
* @return array
|
||||
* Test cases.
|
||||
*/
|
||||
public static function providerTestFormatAgnosticNormalizers() {
|
||||
return [
|
||||
'Format-agnostic @FieldType-level normalizers SHOULD NOT be able to affect the JSON:API normalization' => [
|
||||
'jsonapi_test_field_type',
|
||||
// \Drupal\jsonapi_test_field_type\Normalizer\StringNormalizer::normalize()
|
||||
static::VALUE_ORIGINAL,
|
||||
// \Drupal\jsonapi_test_field_type\Normalizer\StringNormalizer::denormalize()
|
||||
static::VALUE_OVERRIDDEN,
|
||||
],
|
||||
'Format-agnostic @DataType-level normalizers SHOULD be able to affect the JSON:API normalization' => [
|
||||
'jsonapi_test_data_type',
|
||||
// \Drupal\jsonapi_test_data_type\Normalizer\StringNormalizer::normalize()
|
||||
static::VALUE_OVERRIDDEN,
|
||||
// \Drupal\jsonapi_test_data_type\Normalizer\StringNormalizer::denormalize()
|
||||
static::VALUE_ORIGINAL,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "FieldConfig" config entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class FieldConfigTest extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['field', 'node', 'field_ui'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'field_config';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'field_config--field_config';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\field\FieldConfigInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['administer node fields']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
$camelids = NodeType::create([
|
||||
'name' => 'Camelids',
|
||||
'type' => 'camelids',
|
||||
]);
|
||||
$camelids->save();
|
||||
|
||||
$field_storage = FieldStorageConfig::create([
|
||||
'field_name' => 'field_llama',
|
||||
'entity_type' => 'node',
|
||||
'type' => 'text',
|
||||
]);
|
||||
$field_storage->save();
|
||||
|
||||
$entity = FieldConfig::create([
|
||||
'field_storage' => $field_storage,
|
||||
'bundle' => 'camelids',
|
||||
]);
|
||||
$entity->save();
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/field_config/field_config/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'field_config--field_config',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'bundle' => 'camelids',
|
||||
'default_value' => [],
|
||||
'default_value_callback' => '',
|
||||
'dependencies' => [
|
||||
'config' => [
|
||||
'field.storage.node.field_llama',
|
||||
'node.type.camelids',
|
||||
],
|
||||
'module' => [
|
||||
'text',
|
||||
],
|
||||
],
|
||||
'description' => '',
|
||||
'entity_type' => 'node',
|
||||
'field_name' => 'field_llama',
|
||||
'field_type' => 'text',
|
||||
'label' => 'field_llama',
|
||||
'langcode' => 'en',
|
||||
'required' => FALSE,
|
||||
'settings' => ['allowed_formats' => []],
|
||||
'status' => TRUE,
|
||||
'translatable' => TRUE,
|
||||
'drupal_internal__id' => 'node.camelids.field_llama',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
// @todo Update in https://www.drupal.org/node/2300677.
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessMessage($method): string {
|
||||
return "The 'administer node fields' permission is required.";
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createAnotherEntity($key) {
|
||||
NodeType::create([
|
||||
'name' => 'Pachyderms',
|
||||
'type' => 'pachyderms',
|
||||
])->save();
|
||||
|
||||
$field_storage = FieldStorageConfig::create([
|
||||
'field_name' => 'field_pachyderm',
|
||||
'entity_type' => 'node',
|
||||
'type' => 'text',
|
||||
]);
|
||||
$field_storage->save();
|
||||
|
||||
$entity = FieldConfig::create([
|
||||
'field_storage' => $field_storage,
|
||||
'bundle' => 'pachyderms',
|
||||
]);
|
||||
$entity->save();
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static function entityAccess(EntityInterface $entity, $operation, AccountInterface $account) {
|
||||
// Also clear the 'field_storage_config' entity access handler cache because
|
||||
// the 'field_config' access handler delegates access to it.
|
||||
// @see \Drupal\field\FieldConfigAccessControlHandler::checkAccess()
|
||||
\Drupal::entityTypeManager()->getAccessControlHandler('field_storage_config')->resetCache();
|
||||
return parent::entityAccess($entity, $operation, $account);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "FieldStorageConfig" config entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class FieldStorageConfigTest extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['node', 'field_ui'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'field_storage_config';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'field_storage_config--field_storage_config';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\field\FieldConfigStorage
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['administer node fields']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
$field_storage = FieldStorageConfig::create([
|
||||
'field_name' => 'true_llama',
|
||||
'entity_type' => 'node',
|
||||
'type' => 'boolean',
|
||||
]);
|
||||
$field_storage->save();
|
||||
return $field_storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/field_storage_config/field_storage_config/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'field_storage_config--field_storage_config',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'cardinality' => 1,
|
||||
'custom_storage' => FALSE,
|
||||
'dependencies' => [
|
||||
'module' => [
|
||||
'node',
|
||||
],
|
||||
],
|
||||
'entity_type' => 'node',
|
||||
'field_name' => 'true_llama',
|
||||
'indexes' => [],
|
||||
'langcode' => 'en',
|
||||
'locked' => FALSE,
|
||||
'module' => 'core',
|
||||
'persist_with_no_fields' => FALSE,
|
||||
'settings' => [],
|
||||
'status' => TRUE,
|
||||
'translatable' => TRUE,
|
||||
'field_storage_config_type' => 'boolean',
|
||||
'drupal_internal__id' => 'node.true_llama',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
// @todo Update in https://www.drupal.org/node/2300677.
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessMessage($method): string {
|
||||
return "The 'administer node fields' permission is required.";
|
||||
}
|
||||
|
||||
}
|
||||
260
web/core/modules/jsonapi/tests/src/Functional/FileTest.php
Normal file
260
web/core/modules/jsonapi/tests/src/Functional/FileTest.php
Normal file
@ -0,0 +1,260 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\file\Entity\File;
|
||||
use Drupal\file\FileInterface;
|
||||
use Drupal\Tests\jsonapi\Traits\CommonCollectionFilterAccessTestPatternsTrait;
|
||||
use Drupal\user\Entity\User;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "File" content entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class FileTest extends ResourceTestBase {
|
||||
|
||||
use CommonCollectionFilterAccessTestPatternsTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['file', 'user'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'file';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'file--file';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\file\FileInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $patchProtectedFieldNames = [
|
||||
'uri' => NULL,
|
||||
'filemime' => NULL,
|
||||
'filesize' => NULL,
|
||||
'status' => NULL,
|
||||
'changed' => NULL,
|
||||
];
|
||||
|
||||
/**
|
||||
* The file author.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $author;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
$this->grantPermissionsToTestedRole(['access content']);
|
||||
break;
|
||||
|
||||
case 'PATCH':
|
||||
// \Drupal\file\FileAccessControlHandler::checkAccess() grants 'update'
|
||||
// access only to the user that owns the file. So there is no permission
|
||||
// to grant: instead, the file owner must be changed from its default
|
||||
// (user 1) to the current user.
|
||||
$this->makeCurrentUserFileOwner();
|
||||
return;
|
||||
|
||||
case 'DELETE':
|
||||
$this->grantPermissionsToTestedRole(['delete any file']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the current user the file owner.
|
||||
*/
|
||||
protected function makeCurrentUserFileOwner(): void {
|
||||
$account = User::load(2);
|
||||
$this->entity->setOwnerId($account->id());
|
||||
$this->entity->setOwner($account);
|
||||
$this->entity->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
$this->author = User::load(1);
|
||||
|
||||
$file = File::create();
|
||||
$file->setOwnerId($this->author->id());
|
||||
$file->setFilename('drupal.txt');
|
||||
$file->setMimeType('text/plain');
|
||||
$file->setFileUri('public://drupal.txt');
|
||||
$file->set('status', FileInterface::STATUS_PERMANENT);
|
||||
$file->save();
|
||||
|
||||
file_put_contents($file->getFileUri(), 'Drupal');
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createAnotherEntity($key) {
|
||||
/** @var \Drupal\file\FileInterface $duplicate */
|
||||
$duplicate = parent::createAnotherEntity($key);
|
||||
$duplicate->setFileUri("public://$key.txt");
|
||||
$duplicate->save();
|
||||
return $duplicate;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/file/file/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'file--file',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'created' => (new \DateTime())->setTimestamp($this->entity->getCreatedTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
|
||||
'changed' => (new \DateTime())->setTimestamp($this->entity->getChangedTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
|
||||
'filemime' => 'text/plain',
|
||||
'filename' => 'drupal.txt',
|
||||
'filesize' => (int) $this->entity->getSize(),
|
||||
'langcode' => 'en',
|
||||
'status' => TRUE,
|
||||
'uri' => [
|
||||
'url' => base_path() . $this->siteDirectory . '/files/drupal.txt',
|
||||
'value' => 'public://drupal.txt',
|
||||
],
|
||||
'drupal_internal__fid' => 1,
|
||||
],
|
||||
'relationships' => [
|
||||
'uid' => [
|
||||
'data' => [
|
||||
'id' => $this->author->uuid(),
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => (int) $this->author->id(),
|
||||
],
|
||||
'type' => 'user--user',
|
||||
],
|
||||
'links' => [
|
||||
'related' => ['href' => $self_url . '/uid'],
|
||||
'self' => ['href' => $self_url . '/relationships/uid'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
return [
|
||||
'data' => [
|
||||
'type' => 'file--file',
|
||||
'attributes' => [
|
||||
'filename' => 'drupal.txt',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests POST/PATCH/DELETE for an individual resource.
|
||||
*/
|
||||
public function testIndividual(): void {
|
||||
// @todo https://www.drupal.org/node/1927648
|
||||
// Add doTestPostIndividual().
|
||||
$this->doTestPatchIndividual();
|
||||
$this->entity = $this->resaveEntity($this->entity, $this->account);
|
||||
$this->revokePermissions();
|
||||
$this->config('jsonapi.settings')->set('read_only', TRUE)->save(TRUE);
|
||||
$this->doTestDeleteIndividual();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessMessage($method) {
|
||||
return match($method) {
|
||||
'GET' => "The 'access content' permission is required.",
|
||||
'PATCH' => "Only the file owner can update the file entity.",
|
||||
'DELETE' => "The 'delete any file' permission is required.",
|
||||
default => parent::getExpectedUnauthorizedAccessMessage($method),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function testCollectionFilterAccess(): void {
|
||||
$label_field_name = 'filename';
|
||||
// Verify the expected behavior in the common case: when the file is public.
|
||||
$this->doTestCollectionFilterAccessBasedOnPermissions($label_field_name, 'access content');
|
||||
|
||||
$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());
|
||||
|
||||
// 1 result because the current user is the file owner, even though the file
|
||||
// is private.
|
||||
$this->entity->setFileUri('private://drupal.txt');
|
||||
$this->entity->setOwner($this->account);
|
||||
$this->entity->save();
|
||||
$response = $this->request('GET', $collection_filter_url, $request_options);
|
||||
$doc = $this->getDocumentFromResponse($response);
|
||||
$this->assertCount(1, $doc['data']);
|
||||
|
||||
// 0 results because the current user is no longer the file owner and the
|
||||
// file is private.
|
||||
$this->entity->setOwner(User::load(0));
|
||||
$this->entity->save();
|
||||
$response = $this->request('GET', $collection_filter_url, $request_options);
|
||||
$doc = $this->getDocumentFromResponse($response);
|
||||
$this->assertCount(0, $doc['data']);
|
||||
}
|
||||
|
||||
}
|
||||
893
web/core/modules/jsonapi/tests/src/Functional/FileUploadTest.php
Normal file
893
web/core/modules/jsonapi/tests/src/Functional/FileUploadTest.php
Normal file
@ -0,0 +1,893 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Component\Render\PlainTextOutput;
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\entity_test\Entity\EntityTest;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\file\Entity\File;
|
||||
use Drupal\user\Entity\User;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
// cspell:ignore èxample msword
|
||||
|
||||
/**
|
||||
* Tests binary data file upload route.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class FileUploadTest extends ResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['entity_test', 'file'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see $entity
|
||||
*/
|
||||
protected static $entityTypeId = 'entity_test';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see $entity
|
||||
*/
|
||||
protected static $resourceTypeName = 'entity_test--entity_test';
|
||||
|
||||
/**
|
||||
* The POST URI.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $postUri = '/jsonapi/entity_test/entity_test/field_rest_file_test';
|
||||
|
||||
/**
|
||||
* Test file data.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $testFileData = 'Hares sit on chairs, and mules sit on stools.';
|
||||
|
||||
/**
|
||||
* The test field storage config.
|
||||
*
|
||||
* @var \Drupal\field\Entity\FieldStorageConfig
|
||||
*/
|
||||
protected $fieldStorage;
|
||||
|
||||
/**
|
||||
* The field config.
|
||||
*
|
||||
* @var \Drupal\field\Entity\FieldConfig
|
||||
*/
|
||||
protected $field;
|
||||
|
||||
/**
|
||||
* The parent entity.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* Created file entity.
|
||||
*
|
||||
* @var \Drupal\file\Entity\File
|
||||
*/
|
||||
protected $file;
|
||||
|
||||
/**
|
||||
* An authenticated user.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $user;
|
||||
|
||||
/**
|
||||
* The entity storage for the 'file' entity type.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityStorageInterface
|
||||
*/
|
||||
protected $fileStorage;
|
||||
|
||||
/**
|
||||
* A list of test methods to skip.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
const SKIP_METHODS = ['testGetIndividual', 'testIndividual', 'testCollection', 'testRelationships'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
if (in_array($this->name(), static::SKIP_METHODS, TRUE)) {
|
||||
$this->markTestSkipped('Irrelevant for this test');
|
||||
}
|
||||
|
||||
parent::setUp();
|
||||
|
||||
$this->fileStorage = $this->container->get('entity_type.manager')
|
||||
->getStorage('file');
|
||||
|
||||
// Add a file field.
|
||||
$this->fieldStorage = FieldStorageConfig::create([
|
||||
'entity_type' => 'entity_test',
|
||||
'field_name' => 'field_rest_file_test',
|
||||
'type' => 'file',
|
||||
'settings' => [
|
||||
'uri_scheme' => 'public',
|
||||
],
|
||||
])
|
||||
->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
|
||||
$this->fieldStorage->save();
|
||||
|
||||
$this->field = FieldConfig::create([
|
||||
'entity_type' => 'entity_test',
|
||||
'field_name' => 'field_rest_file_test',
|
||||
'bundle' => 'entity_test',
|
||||
'settings' => [
|
||||
'file_directory' => 'foobar',
|
||||
'file_extensions' => 'txt',
|
||||
'max_filesize' => '',
|
||||
],
|
||||
])
|
||||
->setLabel('Test file field')
|
||||
->setTranslatable(FALSE);
|
||||
$this->field->save();
|
||||
|
||||
// Reload entity so that it has the new field.
|
||||
$this->entity = $this->entityStorage->loadUnchanged($this->entity->id());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
// Create an entity that a file can be attached to.
|
||||
$entity_test = EntityTest::create([
|
||||
'name' => 'Llama',
|
||||
'type' => 'entity_test',
|
||||
]);
|
||||
$entity_test->setOwnerId($this->account->id());
|
||||
$entity_test->save();
|
||||
|
||||
return $entity_test;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function testRelated(): void {
|
||||
\Drupal::service('router.builder')->rebuild();
|
||||
parent::testRelated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests using the file upload POST route; needs second request to "use" file.
|
||||
*/
|
||||
public function testPostFileUpload(): void {
|
||||
\Drupal::service('router.builder')->rebuild();
|
||||
$uri = Url::fromUri('base:' . static::$postUri);
|
||||
|
||||
// DX: 405 when read-only mode is enabled.
|
||||
$response = $this->fileRequest($uri, $this->testFileData);
|
||||
$this->assertResourceErrorResponse(405, sprintf("JSON:API is configured to accept only read operations. Site administrators can configure this at %s.", Url::fromUri('base:/admin/config/services/jsonapi')->setAbsolute()->toString(TRUE)->getGeneratedUrl()), $uri, $response);
|
||||
$this->assertSame(['GET'], $response->getHeader('Allow'));
|
||||
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
|
||||
// DX: 403 when unauthorized.
|
||||
$response = $this->fileRequest($uri, $this->testFileData);
|
||||
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('POST'), $uri, $response);
|
||||
|
||||
$this->setUpAuthorization('POST');
|
||||
|
||||
// 404 when the field name is invalid.
|
||||
$invalid_uri = Url::fromUri('base:' . static::$postUri . '_invalid');
|
||||
$response = $this->fileRequest($invalid_uri, $this->testFileData);
|
||||
$this->assertResourceErrorResponse(404, 'Field "field_rest_file_test_invalid" does not exist.', $invalid_uri, $response);
|
||||
|
||||
// This request will have the default 'application/octet-stream' content
|
||||
// type header.
|
||||
$response = $this->fileRequest($uri, $this->testFileData);
|
||||
$this->assertSame(201, $response->getStatusCode());
|
||||
$expected = $this->getExpectedDocument();
|
||||
$this->assertResponseData($expected, $response);
|
||||
|
||||
// Check the actual file data.
|
||||
$this->assertSame($this->testFileData, file_get_contents('public://foobar/example.txt'));
|
||||
|
||||
// Test the file again but using 'filename' in the Content-Disposition
|
||||
// header with no 'file' prefix.
|
||||
$response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'filename="example.txt"']);
|
||||
$this->assertSame(201, $response->getStatusCode());
|
||||
$expected = $this->getExpectedDocument(2, 'example_0.txt', TRUE);
|
||||
$this->assertResponseData($expected, $response);
|
||||
|
||||
// Check the actual file data.
|
||||
$this->assertSame($this->testFileData, file_get_contents('public://foobar/example_0.txt'));
|
||||
$this->assertTrue($this->fileStorage->loadUnchanged(1)->isTemporary());
|
||||
|
||||
// Verify that we can create an entity that references the uploaded file.
|
||||
$entity_test_post_url = Url::fromRoute('jsonapi.entity_test--entity_test.collection.post');
|
||||
$request_options = [];
|
||||
$request_options[RequestOptions::HEADERS]['Content-Type'] = 'application/vnd.api+json';
|
||||
$request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions());
|
||||
|
||||
$request_options[RequestOptions::BODY] = Json::encode($this->getPostDocument());
|
||||
$response = $this->request('POST', $entity_test_post_url, $request_options);
|
||||
$this->assertResourceResponse(201, FALSE, $response);
|
||||
$this->assertTrue($this->fileStorage->loadUnchanged(1)->isPermanent());
|
||||
$this->assertSame([
|
||||
[
|
||||
'target_id' => '1',
|
||||
'display' => NULL,
|
||||
'description' => "The most fascinating file ever!",
|
||||
],
|
||||
], EntityTest::load(2)->get('field_rest_file_test')->getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests using the 'file upload and "use" file in single request" POST route.
|
||||
*/
|
||||
public function testPostFileUploadAndUseInSingleRequest(): void {
|
||||
\Drupal::service('router.builder')->rebuild();
|
||||
// Update the test entity so it already has a file. This allows verifying
|
||||
// that this route appends files, and does not replace them.
|
||||
mkdir('public://foobar');
|
||||
file_put_contents('public://foobar/existing.txt', $this->testFileData);
|
||||
$existing_file = File::create([
|
||||
'uri' => 'public://foobar/existing.txt',
|
||||
]);
|
||||
$existing_file->setOwnerId($this->account->id());
|
||||
$existing_file->setPermanent();
|
||||
$existing_file->save();
|
||||
$this->entity
|
||||
->set('field_rest_file_test', ['target_id' => $existing_file->id()])
|
||||
->save();
|
||||
|
||||
$uri = Url::fromUri('base:/jsonapi/entity_test/entity_test/' . $this->entity->uuid() . '/field_rest_file_test');
|
||||
|
||||
// DX: 405 when read-only mode is enabled.
|
||||
$response = $this->fileRequest($uri, $this->testFileData);
|
||||
$this->assertResourceErrorResponse(405, sprintf("JSON:API is configured to accept only read operations. Site administrators can configure this at %s.", Url::fromUri('base:/admin/config/services/jsonapi')->setAbsolute()->toString(TRUE)->getGeneratedUrl()), $uri, $response);
|
||||
$this->assertSame(['GET'], $response->getHeader('Allow'));
|
||||
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
|
||||
// DX: 403 when unauthorized.
|
||||
$response = $this->fileRequest($uri, $this->testFileData);
|
||||
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('PATCH'), $uri, $response);
|
||||
|
||||
$this->setUpAuthorization('PATCH');
|
||||
|
||||
// 404 when the field name is invalid.
|
||||
$invalid_uri = Url::fromUri($uri->getUri() . '_invalid');
|
||||
$response = $this->fileRequest($invalid_uri, $this->testFileData);
|
||||
$this->assertResourceErrorResponse(404, 'Field "field_rest_file_test_invalid" does not exist.', $invalid_uri, $response);
|
||||
|
||||
// This request fails despite the upload succeeding, because we're not
|
||||
// allowed to view the entity we're uploading to.
|
||||
$response = $this->fileRequest($uri, $this->testFileData);
|
||||
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $uri, $response, FALSE, ['4xx-response', 'http_response'], ['url.query_args', 'url.site', 'user.permissions']);
|
||||
|
||||
$this->setUpAuthorization('GET');
|
||||
|
||||
// Re-uploading the same file will result in the file being uploaded twice
|
||||
// and referenced twice.
|
||||
$response = $this->fileRequest($uri, $this->testFileData);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
$expected = [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => Url::fromUri('base:/jsonapi/entity_test/entity_test/' . $this->entity->uuid() . '/field_rest_file_test')->setAbsolute(TRUE)->toString()],
|
||||
],
|
||||
'data' => [
|
||||
0 => $this->getExpectedDocument(1, 'existing.txt', TRUE, TRUE)['data'],
|
||||
1 => $this->getExpectedDocument(2, 'example.txt', TRUE, TRUE)['data'],
|
||||
2 => $this->getExpectedDocument(3, 'example_0.txt', TRUE, TRUE)['data'],
|
||||
],
|
||||
];
|
||||
$this->assertResponseData($expected, $response);
|
||||
|
||||
// The response document received for the POST request is identical to the
|
||||
// response document received by GETting the same URL.
|
||||
$request_options = [];
|
||||
$request_options[RequestOptions::HEADERS]['Content-Type'] = 'application/vnd.api+json';
|
||||
$request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions());
|
||||
$response = $this->request('GET', $uri, $request_options);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
$this->assertResponseData($expected, $response);
|
||||
|
||||
// Check the actual file data.
|
||||
$this->assertSame($this->testFileData, file_get_contents('public://foobar/example.txt'));
|
||||
$this->assertSame($this->testFileData, file_get_contents('public://foobar/example_0.txt'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON:API POST document referencing the uploaded file.
|
||||
*
|
||||
* @return array
|
||||
* A JSON:API request document.
|
||||
*
|
||||
* @see ::testPostFileUpload()
|
||||
* @see \Drupal\Tests\jsonapi\Functional\EntityTestTest::getPostDocument()
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
return [
|
||||
'data' => [
|
||||
'type' => 'entity_test--entity_test',
|
||||
'attributes' => [
|
||||
'name' => 'Drama llama',
|
||||
],
|
||||
'relationships' => [
|
||||
'field_rest_file_test' => [
|
||||
'data' => [
|
||||
'id' => File::load(1)->uuid(),
|
||||
'meta' => [
|
||||
'description' => 'The most fascinating file ever!',
|
||||
],
|
||||
'type' => 'file--file',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests using the file upload POST route with invalid headers.
|
||||
*/
|
||||
protected function testPostFileUploadInvalidHeaders(): void {
|
||||
$uri = Url::fromUri('base:' . static::$postUri);
|
||||
|
||||
// The wrong content type header should return a 415 code.
|
||||
$response = $this->fileRequest($uri, $this->testFileData, ['Content-Type' => 'application/vnd.api+json']);
|
||||
$this->assertSame(415, $response->getStatusCode());
|
||||
|
||||
// An empty Content-Disposition header should return a 400.
|
||||
$response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => FALSE]);
|
||||
$this->assertResourceErrorResponse(400, '"Content-Disposition" header is required. A file name in the format "filename=FILENAME" must be provided.', $uri, $response);
|
||||
|
||||
// An empty filename with a context in the Content-Disposition header should
|
||||
// return a 400.
|
||||
$response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'file; filename=""']);
|
||||
$this->assertResourceErrorResponse(400, 'No filename found in "Content-Disposition" header. A file name in the format "filename=FILENAME" must be provided.', $uri, $response);
|
||||
|
||||
// An empty filename without a context in the Content-Disposition header
|
||||
// should return a 400.
|
||||
$response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'filename=""']);
|
||||
$this->assertResourceErrorResponse(400, 'No filename found in "Content-Disposition" header. A file name in the format "filename=FILENAME" must be provided.', $uri, $response);
|
||||
|
||||
// An invalid key-value pair in the Content-Disposition header should return
|
||||
// a 400.
|
||||
$response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'not_a_filename="example.txt"']);
|
||||
$this->assertResourceErrorResponse(400, 'No filename found in "Content-Disposition" header. A file name in the format "filename=FILENAME" must be provided.', $uri, $response);
|
||||
|
||||
// Using filename* extended format is not currently supported.
|
||||
$response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'filename*="UTF-8 \' \' example.txt"']);
|
||||
$this->assertResourceErrorResponse(400, 'The extended "filename*" format is currently not supported in the "Content-Disposition" header.', $uri, $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests using the file upload POST route with a duplicate file name.
|
||||
*
|
||||
* A new file should be created with a suffixed name.
|
||||
*/
|
||||
public function testPostFileUploadDuplicateFile(): void {
|
||||
\Drupal::service('router.builder')->rebuild();
|
||||
$this->setUpAuthorization('POST');
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
|
||||
$uri = Url::fromUri('base:' . static::$postUri);
|
||||
|
||||
// This request will have the default 'application/octet-stream' content
|
||||
// type header.
|
||||
$response = $this->fileRequest($uri, $this->testFileData);
|
||||
|
||||
$this->assertSame(201, $response->getStatusCode());
|
||||
|
||||
// Make the same request again. The file should be saved as a new file
|
||||
// entity that has the same file name but a suffixed file URI.
|
||||
$response = $this->fileRequest($uri, $this->testFileData);
|
||||
$this->assertSame(201, $response->getStatusCode());
|
||||
|
||||
// Loading expected normalized data for file 2, the duplicate file.
|
||||
$expected = $this->getExpectedDocument(2, 'example_0.txt', TRUE);
|
||||
$this->assertResponseData($expected, $response);
|
||||
|
||||
// Check the actual file data.
|
||||
$this->assertSame($this->testFileData, file_get_contents('public://foobar/example_0.txt'));
|
||||
|
||||
// Simulate a race condition where two files are uploaded at almost the same
|
||||
// time, by removing the first uploaded file from disk (leaving the entry in
|
||||
// the file_managed table) before trying to upload another file with the
|
||||
// same name.
|
||||
unlink(\Drupal::service('file_system')->realpath('public://foobar/example.txt'));
|
||||
|
||||
// Make the same request again. The upload should fail validation.
|
||||
$response = $this->fileRequest($uri, $this->testFileData);
|
||||
$this->assertResourceErrorResponse(422, PlainTextOutput::renderFromHtml("Unprocessable Entity: file validation failed.\nThe file public://foobar/example.txt already exists. Enter a unique file URI."), $uri, $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests using the file upload route with any path prefixes being stripped.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#Directives
|
||||
*/
|
||||
public function testFileUploadStrippedFilePath(): void {
|
||||
\Drupal::service('router.builder')->rebuild();
|
||||
$this->setUpAuthorization('POST');
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
|
||||
$uri = Url::fromUri('base:' . static::$postUri);
|
||||
|
||||
$response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'file; filename="directory/example.txt"']);
|
||||
$this->assertSame(201, $response->getStatusCode());
|
||||
$expected = $this->getExpectedDocument();
|
||||
$this->assertResponseData($expected, $response);
|
||||
|
||||
// Check the actual file data. It should have been written to the configured
|
||||
// directory, not /foobar/directory/example.txt.
|
||||
$this->assertSame($this->testFileData, file_get_contents('public://foobar/example.txt'));
|
||||
|
||||
$response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'file; filename="../../example_2.txt"']);
|
||||
$this->assertSame(201, $response->getStatusCode());
|
||||
$expected = $this->getExpectedDocument(2, 'example_2.txt', TRUE);
|
||||
$this->assertResponseData($expected, $response);
|
||||
|
||||
// Check the actual file data. It should have been written to the configured
|
||||
// directory, not /foobar/directory/example.txt.
|
||||
$this->assertSame($this->testFileData, file_get_contents('public://foobar/example_2.txt'));
|
||||
$this->assertFileDoesNotExist('../../example_2.txt');
|
||||
|
||||
// Check a path from the root. Extensions have to be empty to allow a file
|
||||
// with no extension to pass validation.
|
||||
$this->field->setSetting('file_extensions', '')
|
||||
->save();
|
||||
\Drupal::service('router.builder')->rebuild();
|
||||
|
||||
$response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'file; filename="/etc/passwd"']);
|
||||
$this->assertSame(201, $response->getStatusCode());
|
||||
$expected = $this->getExpectedDocument(3, 'passwd', TRUE);
|
||||
// This mime will be guessed as there is no extension.
|
||||
$expected['data']['attributes']['filemime'] = 'application/octet-stream';
|
||||
$this->assertResponseData($expected, $response);
|
||||
|
||||
// Check the actual file data. It should have been written to the configured
|
||||
// directory, not /foobar/directory/example.txt.
|
||||
$this->assertSame($this->testFileData, file_get_contents('public://foobar/passwd'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests invalid file uploads.
|
||||
*/
|
||||
public function testInvalidFileUploads(): void {
|
||||
\Drupal::service('router.builder')->rebuild();
|
||||
$this->setUpAuthorization('POST');
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
$this->testFileUploadInvalidFileType();
|
||||
$this->testPostFileUploadInvalidHeaders();
|
||||
$this->testFileUploadLargerFileSize();
|
||||
$this->testFileUploadMaliciousExtension();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests using the file upload route with a unicode file name.
|
||||
*/
|
||||
public function testFileUploadUnicodeFilename(): void {
|
||||
\Drupal::service('router.builder')->rebuild();
|
||||
$this->setUpAuthorization('POST');
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
|
||||
$uri = Url::fromUri('base:' . static::$postUri);
|
||||
|
||||
// It is important that the filename starts with a unicode character. See
|
||||
// https://bugs.php.net/bug.php?id=77239.
|
||||
$response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'file; filename="Èxample-✓.txt"']);
|
||||
$this->assertSame(201, $response->getStatusCode());
|
||||
$expected = $this->getExpectedDocument(1, 'Èxample-✓.txt', TRUE);
|
||||
$this->assertResponseData($expected, $response);
|
||||
$this->assertSame($this->testFileData, file_get_contents('public://foobar/Èxample-✓.txt'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests using the file upload route with a zero byte file.
|
||||
*/
|
||||
public function testFileUploadZeroByteFile(): void {
|
||||
\Drupal::service('router.builder')->rebuild();
|
||||
$this->setUpAuthorization('POST');
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
|
||||
$uri = Url::fromUri('base:' . static::$postUri);
|
||||
|
||||
// Test with a zero byte file.
|
||||
$response = $this->fileRequest($uri, NULL);
|
||||
$this->assertSame(201, $response->getStatusCode());
|
||||
$expected = $this->getExpectedDocument();
|
||||
// Modify the default expected data to account for the 0 byte file.
|
||||
$expected['data']['attributes']['filesize'] = 0;
|
||||
$this->assertResponseData($expected, $response);
|
||||
|
||||
// Check the actual file data.
|
||||
$this->assertSame('', file_get_contents('public://foobar/example.txt'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests using the file upload route with an invalid file type.
|
||||
*/
|
||||
protected function testFileUploadInvalidFileType(): void {
|
||||
$uri = Url::fromUri('base:' . static::$postUri);
|
||||
|
||||
// Test with a JSON file.
|
||||
$response = $this->fileRequest($uri, '{"test":123}', ['Content-Disposition' => 'filename="example.json"']);
|
||||
$this->assertResourceErrorResponse(422, PlainTextOutput::renderFromHtml("Unprocessable Entity: file validation failed.\nOnly files with the following extensions are allowed: <em class=\"placeholder\">txt</em>."), $uri, $response);
|
||||
|
||||
// Make sure that no file was saved.
|
||||
$this->assertEmpty(File::load(1));
|
||||
$this->assertFileDoesNotExist('public://foobar/example.txt');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests using the file upload route with a file size larger than allowed.
|
||||
*/
|
||||
protected function testFileUploadLargerFileSize(): void {
|
||||
// Set a limit of 50 bytes.
|
||||
$this->field->setSetting('max_filesize', 50)
|
||||
->save();
|
||||
\Drupal::service('router.builder')->rebuild();
|
||||
$uri = Url::fromUri('base:' . static::$postUri);
|
||||
|
||||
// Generate a string larger than the 50 byte limit set.
|
||||
$response = $this->fileRequest($uri, $this->randomString(100));
|
||||
$this->assertResourceErrorResponse(422, PlainTextOutput::renderFromHtml("Unprocessable Entity: file validation failed.\nThe file is <em class=\"placeholder\">100 bytes</em> exceeding the maximum file size of <em class=\"placeholder\">50 bytes</em>."), $uri, $response);
|
||||
|
||||
// Make sure that no file was saved.
|
||||
$this->assertEmpty(File::load(1));
|
||||
$this->assertFileDoesNotExist('public://foobar/example.txt');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests using the file upload POST route with malicious extensions.
|
||||
*/
|
||||
protected function testFileUploadMaliciousExtension(): void {
|
||||
// Allow all file uploads but system.file::allow_insecure_uploads is set to
|
||||
// FALSE.
|
||||
$this->field->setSetting('file_extensions', '')->save();
|
||||
|
||||
$uri = Url::fromUri('base:' . static::$postUri);
|
||||
|
||||
$php_string = '<?php print "Drupal"; ?>';
|
||||
|
||||
// Test using a masked exploit file.
|
||||
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example.php"']);
|
||||
// The filename is not munged because .txt is added and it is a known
|
||||
// extension to apache.
|
||||
$expected = $this->getExpectedDocument(1, 'example.php_.txt', TRUE);
|
||||
// Override the expected filesize.
|
||||
$expected['data']['attributes']['filesize'] = strlen($php_string);
|
||||
$this->assertResponseData($expected, $response);
|
||||
$this->assertFileExists('public://foobar/example.php_.txt');
|
||||
|
||||
// Add .php and .txt as allowed extensions. Since 'allow_insecure_uploads'
|
||||
// is FALSE, .php files should be renamed to have a .txt extension.
|
||||
$this->field->setSetting('file_extensions', 'php txt')->save();
|
||||
|
||||
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_2.php"']);
|
||||
$expected = $this->getExpectedDocument(2, 'example_2.php_.txt', TRUE);
|
||||
// Override the expected filesize.
|
||||
$expected['data']['attributes']['filesize'] = strlen($php_string);
|
||||
$this->assertResponseData($expected, $response);
|
||||
$this->assertFileExists('public://foobar/example_2.php_.txt');
|
||||
$this->assertFileDoesNotExist('public://foobar/example_2.php');
|
||||
|
||||
// Allow .doc file uploads and ensure even a mis-configured apache will not
|
||||
// fallback to php because the filename will be munged.
|
||||
$this->field->setSetting('file_extensions', 'doc')->save();
|
||||
|
||||
// Test using a masked exploit file.
|
||||
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_3.php.doc"']);
|
||||
// The filename is munged.
|
||||
$expected = $this->getExpectedDocument(3, 'example_3.php_.doc', TRUE);
|
||||
// Override the expected filesize.
|
||||
$expected['data']['attributes']['filesize'] = strlen($php_string);
|
||||
// The file mime should be 'application/msword'.
|
||||
$expected['data']['attributes']['filemime'] = 'application/msword';
|
||||
$this->assertResponseData($expected, $response);
|
||||
$this->assertFileExists('public://foobar/example_3.php_.doc');
|
||||
$this->assertFileDoesNotExist('public://foobar/example_3.php.doc');
|
||||
|
||||
// Test that a dangerous extension such as .php is munged even if it is in
|
||||
// the list of allowed extensions.
|
||||
$this->field->setSetting('file_extensions', 'doc php')->save();
|
||||
|
||||
// Test using a masked exploit file.
|
||||
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_4.php.doc"']);
|
||||
// The filename is munged.
|
||||
$expected = $this->getExpectedDocument(4, 'example_4.php_.doc', TRUE);
|
||||
// Override the expected filesize.
|
||||
$expected['data']['attributes']['filesize'] = strlen($php_string);
|
||||
// The file mime should be 'application/msword'.
|
||||
$expected['data']['attributes']['filemime'] = 'application/msword';
|
||||
$this->assertResponseData($expected, $response);
|
||||
$this->assertFileExists('public://foobar/example_4.php_.doc');
|
||||
$this->assertFileDoesNotExist('public://foobar/example_4.php.doc');
|
||||
|
||||
// Dangerous extensions are munged even when all extensions are allowed.
|
||||
$this->field->setSetting('file_extensions', '')->save();
|
||||
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_5.php.png"']);
|
||||
$expected = $this->getExpectedDocument(5, 'example_5.php_.png', TRUE);
|
||||
// Override the expected filesize.
|
||||
$expected['data']['attributes']['filesize'] = strlen($php_string);
|
||||
// The file mime should still see this as a PNG image.
|
||||
$expected['data']['attributes']['filemime'] = 'image/png';
|
||||
$this->assertResponseData($expected, $response);
|
||||
$this->assertFileExists('public://foobar/example_5.php_.png');
|
||||
|
||||
// Dangerous extensions are munged if is renamed to end in .txt.
|
||||
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_6.cgi.png.txt"']);
|
||||
$expected = $this->getExpectedDocument(6, 'example_6.cgi_.png_.txt', TRUE);
|
||||
// Override the expected filesize.
|
||||
$expected['data']['attributes']['filesize'] = strlen($php_string);
|
||||
// The file mime should also now be text.
|
||||
$expected['data']['attributes']['filemime'] = 'text/plain';
|
||||
$this->assertResponseData($expected, $response);
|
||||
$this->assertFileExists('public://foobar/example_6.cgi_.png_.txt');
|
||||
|
||||
// Add .php as an allowed extension without .txt. Since insecure uploads are
|
||||
// are not allowed, .php files will be rejected.
|
||||
$this->field->setSetting('file_extensions', 'php')->save();
|
||||
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_7.php"']);
|
||||
$this->assertResourceErrorResponse(422, "Unprocessable Entity: file validation failed.\nFor security reasons, your upload has been rejected.", $uri, $response);
|
||||
|
||||
// Make sure that no file was saved.
|
||||
$this->assertFileDoesNotExist('public://foobar/example_7.php');
|
||||
$this->assertFileDoesNotExist('public://foobar/example_7.php.txt');
|
||||
|
||||
// Now allow insecure uploads.
|
||||
\Drupal::configFactory()
|
||||
->getEditable('system.file')
|
||||
->set('allow_insecure_uploads', TRUE)
|
||||
->save();
|
||||
// Allow all file uploads. This is very insecure.
|
||||
$this->field->setSetting('file_extensions', '')->save();
|
||||
\Drupal::service('router.builder')->rebuild();
|
||||
|
||||
$response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_7.php"']);
|
||||
$expected = $this->getExpectedDocument(7, 'example_7.php', TRUE);
|
||||
// Override the expected filesize.
|
||||
$expected['data']['attributes']['filesize'] = strlen($php_string);
|
||||
// The file mime should also now be PHP.
|
||||
$expected['data']['attributes']['filemime'] = 'application/x-httpd-php';
|
||||
$this->assertResponseData($expected, $response);
|
||||
$this->assertFileExists('public://foobar/example_7.php');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests using the file upload POST route no configuration.
|
||||
*/
|
||||
public function testFileUploadNoConfiguration(): void {
|
||||
$this->setUpAuthorization('POST');
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
|
||||
$uri = Url::fromUri('base:' . static::$postUri);
|
||||
|
||||
$this->field->setSetting('file_extensions', '')
|
||||
->save();
|
||||
\Drupal::service('router.builder')->rebuild();
|
||||
|
||||
$response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'filename="example.txt"']);
|
||||
$expected = $this->getExpectedDocument(1, 'example.txt', TRUE);
|
||||
|
||||
$this->assertResponseData($expected, $response);
|
||||
$this->assertFileExists('public://foobar/example.txt');
|
||||
$this->field->setSetting('file_directory', '')
|
||||
->save();
|
||||
|
||||
$response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'filename="example.txt"']);
|
||||
$expected = $this->getExpectedDocument(2, 'example.txt', TRUE);
|
||||
$expected['data']['attributes']['uri']['value'] = 'public://example.txt';
|
||||
$expected['data']['attributes']['uri']['url'] = base_path() . $this->siteDirectory . '/files/example.txt';
|
||||
|
||||
$this->assertResponseData($expected, $response);
|
||||
$this->assertFileExists('public://example.txt');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessMessage($method): string {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
return "The current user is not allowed to view this relationship. The 'view test entity' permission is required.";
|
||||
|
||||
case 'POST':
|
||||
return "The current user is not permitted to upload a file for this field. The following permissions are required: 'administer entity_test content' OR 'administer entity_test_with_bundle content' OR 'create entity_test entity_test_with_bundle entities'.";
|
||||
|
||||
case 'PATCH':
|
||||
return "The current user is not permitted to upload a file for this field. The 'administer entity_test content' permission is required.";
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the expected JSON:API document for the expected file entity.
|
||||
*
|
||||
* @param int $fid
|
||||
* The file ID to load and create a JSON:API document for.
|
||||
* @param string $expected_filename
|
||||
* The expected filename for the stored file.
|
||||
* @param bool $expected_as_filename
|
||||
* Whether the expected filename should be the filename property too.
|
||||
* @param bool $expected_status
|
||||
* The expected file status. Defaults to FALSE.
|
||||
*
|
||||
* @return array
|
||||
* A JSON:API response document.
|
||||
*/
|
||||
protected function getExpectedDocument($fid = 1, $expected_filename = 'example.txt', $expected_as_filename = FALSE, $expected_status = FALSE): array {
|
||||
$author = User::load($this->account->id());
|
||||
$file = File::load($fid);
|
||||
$this->assertInstanceOf(File::class, $file);
|
||||
$self_url = Url::fromUri('base:/jsonapi/file/file/' . $file->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $file->uuid(),
|
||||
'type' => 'file--file',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'created' => (new \DateTime())->setTimestamp($file->getCreatedTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
|
||||
'changed' => (new \DateTime())->setTimestamp($file->getChangedTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
|
||||
'filemime' => 'text/plain',
|
||||
'filename' => $expected_as_filename ? $expected_filename : 'example.txt',
|
||||
'filesize' => strlen($this->testFileData),
|
||||
'langcode' => 'en',
|
||||
'status' => $expected_status,
|
||||
'uri' => [
|
||||
'value' => 'public://foobar/' . $expected_filename,
|
||||
'url' => base_path() . $this->siteDirectory . '/files/foobar/' . rawurlencode($expected_filename),
|
||||
],
|
||||
'drupal_internal__fid' => (int) $file->id(),
|
||||
],
|
||||
'relationships' => [
|
||||
'uid' => [
|
||||
'data' => [
|
||||
'id' => $author->uuid(),
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => (int) $author->id(),
|
||||
],
|
||||
'type' => 'user--user',
|
||||
],
|
||||
'links' => [
|
||||
'related' => ['href' => $self_url . '/uid'],
|
||||
'self' => ['href' => $self_url . '/relationships/uid'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a file upload request. Wraps the Guzzle HTTP client.
|
||||
*
|
||||
* @param \Drupal\Core\Url $url
|
||||
* URL to request.
|
||||
* @param string $file_contents
|
||||
* The file contents to send as the request body.
|
||||
* @param array $headers
|
||||
* Additional headers to send with the request. Defaults will be added for
|
||||
* Content-Type and Content-Disposition. In order to remove the defaults set
|
||||
* the header value to FALSE.
|
||||
*
|
||||
* @return \Psr\Http\Message\ResponseInterface
|
||||
* The received response.
|
||||
*
|
||||
* @see \GuzzleHttp\ClientInterface::request()
|
||||
*/
|
||||
protected function fileRequest(Url $url, $file_contents, array $headers = []): ResponseInterface {
|
||||
$request_options = [];
|
||||
$headers = $headers + [
|
||||
// Set the required (and only accepted) content type for the request.
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
// Set the required Content-Disposition header for the file name.
|
||||
'Content-Disposition' => 'file; filename="example.txt"',
|
||||
// Set the required JSON:API Accept header.
|
||||
'Accept' => 'application/vnd.api+json',
|
||||
];
|
||||
$request_options[RequestOptions::HEADERS] = array_filter($headers, function ($value) {
|
||||
return $value !== FALSE;
|
||||
});
|
||||
$request_options[RequestOptions::BODY] = $file_contents;
|
||||
$request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions());
|
||||
|
||||
return $this->request('POST', $url, $request_options);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
$this->grantPermissionsToTestedRole(['view test entity']);
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
$this->grantPermissionsToTestedRole(['create entity_test entity_test_with_bundle entities', 'access content']);
|
||||
break;
|
||||
|
||||
case 'PATCH':
|
||||
$this->grantPermissionsToTestedRole(['administer entity_test content', 'access content']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts expected normalized data matches response data.
|
||||
*
|
||||
* @param array $expected
|
||||
* The expected data.
|
||||
* @param \Psr\Http\Message\ResponseInterface $response
|
||||
* The file upload response.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function assertResponseData(array $expected, ResponseInterface $response): void {
|
||||
static::recursiveKsort($expected);
|
||||
$actual = $this->getDocumentFromResponse($response);
|
||||
static::recursiveKsort($actual);
|
||||
|
||||
$this->assertSame($expected, $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessCacheability() {
|
||||
// There is cacheability metadata to check as file uploads only allows POST
|
||||
// requests, which will not return cacheable responses.
|
||||
return new CacheableMetadata();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "FilterFormat" config entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class FilterFormatTest extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['filter'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'filter_format';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'filter_format--filter_format';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\filter\FilterFormatInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['administer filters']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
$pablo_format = FilterFormat::create([
|
||||
'name' => 'Pablo Picasso',
|
||||
'format' => 'pablo',
|
||||
'langcode' => 'es',
|
||||
'filters' => [
|
||||
'filter_html' => [
|
||||
'status' => TRUE,
|
||||
'settings' => [
|
||||
'allowed_html' => '<p> <a> <b> <lo>',
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
$pablo_format->save();
|
||||
return $pablo_format;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/filter_format/filter_format/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'filter_format--filter_format',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'dependencies' => [],
|
||||
'filters' => [
|
||||
'filter_html' => [
|
||||
'id' => 'filter_html',
|
||||
'provider' => 'filter',
|
||||
'status' => TRUE,
|
||||
'weight' => -10,
|
||||
'settings' => [
|
||||
'allowed_html' => '<p> <a> <b> <lo>',
|
||||
'filter_html_help' => TRUE,
|
||||
'filter_html_nofollow' => FALSE,
|
||||
],
|
||||
],
|
||||
],
|
||||
'langcode' => 'es',
|
||||
'name' => 'Pablo Picasso',
|
||||
'status' => TRUE,
|
||||
'weight' => 0,
|
||||
'drupal_internal__format' => 'pablo',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
// @todo Update in https://www.drupal.org/node/2300677.
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
|
||||
|
||||
/**
|
||||
* Generic module test for jsonapi.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class GenericTest extends GenericModuleTestBase {}
|
||||
139
web/core/modules/jsonapi/tests/src/Functional/ImageStyleTest.php
Normal file
139
web/core/modules/jsonapi/tests/src/Functional/ImageStyleTest.php
Normal file
@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\image\Entity\ImageStyle;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "ImageStyle" config entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class ImageStyleTest extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['image'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'image_style';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'image_style--image_style';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\image\ImageStyleInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* The effect UUID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $effectUuid;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['administer image styles']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
// Create a "Camelids" image style.
|
||||
$camelids = ImageStyle::create([
|
||||
'name' => 'camelids',
|
||||
'label' => 'Camelids',
|
||||
]);
|
||||
|
||||
// Add an image effect.
|
||||
$effect = [
|
||||
'id' => 'image_scale_and_crop',
|
||||
'data' => [
|
||||
'width' => 120,
|
||||
'height' => 121,
|
||||
],
|
||||
'weight' => 0,
|
||||
];
|
||||
$this->effectUuid = $camelids->addImageEffect($effect);
|
||||
|
||||
$camelids->save();
|
||||
|
||||
return $camelids;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/image_style/image_style/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'image_style--image_style',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'dependencies' => [],
|
||||
'effects' => [
|
||||
$this->effectUuid => [
|
||||
'uuid' => $this->effectUuid,
|
||||
'id' => 'image_scale_and_crop',
|
||||
'weight' => 0,
|
||||
'data' => [
|
||||
'anchor' => 'center-center',
|
||||
'width' => 120,
|
||||
'height' => 121,
|
||||
],
|
||||
],
|
||||
],
|
||||
'label' => 'Camelids',
|
||||
'langcode' => 'en',
|
||||
'status' => TRUE,
|
||||
'drupal_internal__name' => 'camelids',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
// @todo Update in https://www.drupal.org/node/2300677.
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,194 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\entity_test\Entity\EntityTestBundle;
|
||||
use Drupal\entity_test\Entity\EntityTestNoLabel;
|
||||
use Drupal\entity_test\Entity\EntityTestWithBundle;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
|
||||
|
||||
/**
|
||||
* Makes assertions about the JSON:API behavior for internal entities.
|
||||
*
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class InternalEntitiesTest extends BrowserTestBase {
|
||||
|
||||
use EntityReferenceFieldCreationTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'jsonapi',
|
||||
'entity_test',
|
||||
'serialization',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* A test user.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $testUser;
|
||||
|
||||
/**
|
||||
* An entity of an internal entity type.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityInterface
|
||||
*/
|
||||
protected $internalEntity;
|
||||
|
||||
/**
|
||||
* An entity referencing an internal entity.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityInterface
|
||||
*/
|
||||
protected $referencingEntity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->testUser = $this->drupalCreateUser([
|
||||
'view test entity',
|
||||
'administer entity_test_with_bundle content',
|
||||
], $this->randomString(), TRUE);
|
||||
EntityTestBundle::create([
|
||||
'id' => 'internal_referencer',
|
||||
'label' => 'Entity Test Internal Referencer',
|
||||
])->save();
|
||||
$this->createEntityReferenceField(
|
||||
'entity_test_with_bundle',
|
||||
'internal_referencer',
|
||||
'field_internal',
|
||||
'Internal Entities',
|
||||
'entity_test_no_label'
|
||||
);
|
||||
$this->internalEntity = EntityTestNoLabel::create([]);
|
||||
$this->internalEntity->save();
|
||||
$this->referencingEntity = EntityTestWithBundle::create([
|
||||
'type' => 'internal_referencer',
|
||||
'field_internal' => $this->internalEntity->id(),
|
||||
]);
|
||||
$this->referencingEntity->save();
|
||||
\Drupal::service('router.builder')->rebuild();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that internal resources types aren't present in the entry point.
|
||||
*/
|
||||
public function testEntryPoint(): void {
|
||||
$document = $this->jsonapiGet('/jsonapi');
|
||||
$this->assertArrayNotHasKey(
|
||||
"{$this->internalEntity->getEntityTypeId()}--{$this->internalEntity->bundle()}",
|
||||
$document['links'],
|
||||
'The entry point should not contain links to internal resource type routes.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that internal resources types aren't present in the routes.
|
||||
*/
|
||||
public function testRoutes(): void {
|
||||
// This cannot be in a data provider because it needs values created by the
|
||||
// setUp method.
|
||||
$paths = [
|
||||
'individual' => "/jsonapi/entity_test_no_label/entity_test_no_label/{$this->internalEntity->uuid()}",
|
||||
'collection' => "/jsonapi/entity_test_no_label/entity_test_no_label",
|
||||
'related' => "/jsonapi/entity_test_no_label/entity_test_no_label/{$this->internalEntity->uuid()}/field_internal",
|
||||
];
|
||||
$this->drupalLogin($this->testUser);
|
||||
foreach ($paths as $path) {
|
||||
$this->drupalGet($path, ['Accept' => 'application/vnd.api+json']);
|
||||
$this->assertSame(404, $this->getSession()->getStatusCode());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that internal entities are not included in compound documents.
|
||||
*/
|
||||
public function testIncludes(): void {
|
||||
$document = $this->getIndividual($this->referencingEntity, [
|
||||
'query' => ['include' => 'field_internal'],
|
||||
]);
|
||||
$this->assertArrayNotHasKey(
|
||||
'included',
|
||||
$document,
|
||||
'Internal entities should not be included in compound documents.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that links to internal relationships aren't generated.
|
||||
*/
|
||||
public function testLinks(): void {
|
||||
$document = $this->getIndividual($this->referencingEntity);
|
||||
$this->assertArrayNotHasKey(
|
||||
'related',
|
||||
$document['data']['relationships']['field_internal']['links'],
|
||||
'Links to internal-only related routes should not be in the document.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the decoded JSON:API document for the for the given entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity to request.
|
||||
* @param array $options
|
||||
* URL options.
|
||||
*
|
||||
* @return array
|
||||
* The decoded response document.
|
||||
*/
|
||||
protected function getIndividual(EntityInterface $entity, array $options = []) {
|
||||
$entity_type_id = $entity->getEntityTypeId();
|
||||
$bundle = $entity->bundle();
|
||||
$path = "/jsonapi/{$entity_type_id}/{$bundle}/{$entity->uuid()}";
|
||||
return $this->jsonapiGet($path, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an authenticated request and returns the decoded document.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity to request.
|
||||
* @param string $relationship
|
||||
* The field name of the relationship to request.
|
||||
* @param array $options
|
||||
* URL options.
|
||||
*
|
||||
* @return array
|
||||
* The decoded response document.
|
||||
*/
|
||||
protected function getRelated(EntityInterface $entity, $relationship, array $options = []) {
|
||||
$entity_type_id = $entity->getEntityTypeId();
|
||||
$bundle = $entity->bundle();
|
||||
$path = "/jsonapi/{$entity_type_id}/{$bundle}/{$entity->uuid()}/{$relationship}";
|
||||
return $this->jsonapiGet($path, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an authenticated request and returns the decoded document.
|
||||
*/
|
||||
protected function jsonapiGet($path, array $options = []) {
|
||||
$this->drupalLogin($this->testUser);
|
||||
$response = $this->drupalGet($path, $options, ['Accept' => 'application/vnd.api+json']);
|
||||
return Json::decode($response);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,241 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\comment\Entity\Comment;
|
||||
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
|
||||
use Drupal\comment\Tests\CommentTestTrait;
|
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\shortcut\Entity\Shortcut;
|
||||
use Drupal\taxonomy\Entity\Term;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
|
||||
// cspell:ignore llamalovers catcuddlers Cuddlers
|
||||
|
||||
/**
|
||||
* JSON:API regression tests.
|
||||
*
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class JsonApiFilterRegressionTest extends JsonApiFunctionalTestBase {
|
||||
|
||||
use CommentTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'basic_auth',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* Ensure filtering on relationships works with bundle-specific target types.
|
||||
*
|
||||
* @see https://www.drupal.org/project/drupal/issues/2953207
|
||||
*/
|
||||
public function testBundleSpecificTargetEntityTypeFromIssue2953207(): void {
|
||||
// Set up data model.
|
||||
$this->assertTrue($this->container->get('module_installer')->install(['comment'], TRUE), 'Installed modules.');
|
||||
$this->addDefaultCommentField('taxonomy_term', 'tags', 'comment', CommentItemInterface::OPEN, 'test_comment_type');
|
||||
$this->rebuildAll();
|
||||
|
||||
// Create data.
|
||||
Term::create([
|
||||
'name' => 'foobar',
|
||||
'vid' => 'tags',
|
||||
])->save();
|
||||
Comment::create([
|
||||
'subject' => 'Llama',
|
||||
'entity_id' => 1,
|
||||
'entity_type' => 'taxonomy_term',
|
||||
'field_name' => 'comment',
|
||||
])->save();
|
||||
|
||||
// Test.
|
||||
$user = $this->drupalCreateUser([
|
||||
'access comments',
|
||||
]);
|
||||
$response = $this->request('GET', Url::fromUri('internal:/jsonapi/comment/test_comment_type?include=entity_id&filter[entity_id.name]=foobar'), [
|
||||
RequestOptions::AUTH => [
|
||||
$user->getAccountName(),
|
||||
$user->pass_raw,
|
||||
],
|
||||
]);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that filtering by a sequential internal ID named 'id' is possible.
|
||||
*
|
||||
* @see https://www.drupal.org/project/drupal/issues/3015759
|
||||
*/
|
||||
public function testFilterByIdFromIssue3015759(): void {
|
||||
// Set up data model.
|
||||
$this->assertTrue($this->container->get('module_installer')->install(['shortcut'], TRUE), 'Installed modules.');
|
||||
$this->rebuildAll();
|
||||
|
||||
// Create data.
|
||||
$shortcut = Shortcut::create([
|
||||
'shortcut_set' => 'default',
|
||||
'title' => $this->randomMachineName(),
|
||||
'weight' => -20,
|
||||
'link' => [
|
||||
'uri' => 'internal:/user/logout',
|
||||
],
|
||||
]);
|
||||
$shortcut->save();
|
||||
|
||||
// Test.
|
||||
$user = $this->drupalCreateUser([
|
||||
'access shortcuts',
|
||||
'customize shortcut links',
|
||||
]);
|
||||
$response = $this->request('GET', Url::fromUri('internal:/jsonapi/shortcut/default?filter[drupal_internal__id]=' . $shortcut->id()), [
|
||||
RequestOptions::AUTH => [
|
||||
$user->getAccountName(),
|
||||
$user->pass_raw,
|
||||
],
|
||||
]);
|
||||
$doc = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
$this->assertNotEmpty($doc['data']);
|
||||
$this->assertSame($doc['data'][0]['id'], $shortcut->uuid());
|
||||
$this->assertSame($doc['data'][0]['attributes']['drupal_internal__id'], (int) $shortcut->id());
|
||||
$this->assertSame($doc['data'][0]['attributes']['title'], $shortcut->label());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure filtering for entities with empty entity reference fields works.
|
||||
*
|
||||
* @see https://www.drupal.org/project/jsonapi/issues/3025372
|
||||
*/
|
||||
public function testEmptyRelationshipFilteringFromIssue3025372(): void {
|
||||
// Set up data model.
|
||||
$this->drupalCreateContentType(['type' => 'folder']);
|
||||
$this->createEntityReferenceField(
|
||||
'node',
|
||||
'folder',
|
||||
'field_parent_folder',
|
||||
NULL,
|
||||
'node',
|
||||
'default',
|
||||
[
|
||||
'target_bundles' => ['folder'],
|
||||
],
|
||||
FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
|
||||
);
|
||||
$this->rebuildAll();
|
||||
|
||||
// Create data.
|
||||
$node = Node::create([
|
||||
'title' => 'root folder',
|
||||
'type' => 'folder',
|
||||
]);
|
||||
$node->save();
|
||||
|
||||
// Test.
|
||||
$user = $this->drupalCreateUser(['access content']);
|
||||
$url = Url::fromRoute('jsonapi.node--folder.collection');
|
||||
$request_options = [
|
||||
RequestOptions::HEADERS => [
|
||||
'Content-Type' => 'application/vnd.api+json',
|
||||
'Accept' => 'application/vnd.api+json',
|
||||
],
|
||||
RequestOptions::AUTH => [$user->getAccountName(), $user->pass_raw],
|
||||
];
|
||||
$response = $this->request('GET', $url, $request_options);
|
||||
$document = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame(200, $response->getStatusCode(), (string) $response->getBody());
|
||||
$this->assertSame($node->uuid(), $document['data'][0]['id']);
|
||||
$response = $this->request('GET', $url->setOption('query', [
|
||||
'filter[test][condition][path]' => 'field_parent_folder',
|
||||
'filter[test][condition][operator]' => 'IS NULL',
|
||||
]), $request_options);
|
||||
$document = $this->getDocumentFromResponse($response);
|
||||
|
||||
$this->assertSame(200, $response->getStatusCode(), (string) $response->getBody());
|
||||
$this->assertSame($node->uuid(), $document['data'][0]['id']);
|
||||
$response = $this->request('GET', $url->setOption('query', [
|
||||
'filter[test][condition][path]' => 'field_parent_folder',
|
||||
'filter[test][condition][operator]' => 'IS NOT NULL',
|
||||
]), $request_options);
|
||||
$document = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame(200, $response->getStatusCode(), (string) $response->getBody());
|
||||
$this->assertEmpty($document['data']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that collections can be filtered by an entity reference target_id.
|
||||
*
|
||||
* @see https://www.drupal.org/project/drupal/issues/3036593
|
||||
*/
|
||||
public function testFilteringEntitiesByEntityReferenceTargetId(): void {
|
||||
// Create two config entities to be the config targets of an entity
|
||||
// reference. In this case, the `roles` field.
|
||||
$role_llamalovers = $this->drupalCreateRole([], 'llamalovers', 'Llama Lovers');
|
||||
$role_catcuddlers = $this->drupalCreateRole([], 'catcuddlers', 'Cat Cuddlers');
|
||||
|
||||
/** @var \Drupal\user\UserInterface[] $users */
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
// Create 3 users, one with the first role and two with the second role.
|
||||
$users[$i] = $this->drupalCreateUser();
|
||||
$users[$i]->addRole($i === 0 ? $role_llamalovers : $role_catcuddlers)
|
||||
->save();
|
||||
// For each user, create a node that is owned by that user. The node's
|
||||
// `uid` field will be used to test filtering by a content entity ID.
|
||||
Node::create([
|
||||
'type' => 'article',
|
||||
'uid' => $users[$i]->id(),
|
||||
'title' => 'Article created by ' . $users[$i]->uuid(),
|
||||
])->save();
|
||||
}
|
||||
|
||||
// Create a user that will be used to execute the test HTTP requests.
|
||||
$account = $this->drupalCreateUser([
|
||||
'administer users',
|
||||
'bypass node access',
|
||||
]);
|
||||
$request_options = [
|
||||
RequestOptions::AUTH => [
|
||||
$account->getAccountName(),
|
||||
$account->pass_raw,
|
||||
],
|
||||
];
|
||||
|
||||
// Ensure that an entity can be filtered by a target machine name.
|
||||
$response = $this->request('GET', Url::fromUri('internal:/jsonapi/user/user?filter[roles.meta.drupal_internal__target_id]=llamalovers'), $request_options);
|
||||
$document = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame(200, $response->getStatusCode(), var_export($document, TRUE));
|
||||
// Only one user should have the first role.
|
||||
$this->assertCount(1, $document['data']);
|
||||
$this->assertSame($users[0]->uuid(), $document['data'][0]['id']);
|
||||
$response = $this->request('GET', Url::fromUri('internal:/jsonapi/user/user?sort=drupal_internal__uid&filter[roles.meta.drupal_internal__target_id]=catcuddlers'), $request_options);
|
||||
$document = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame(200, $response->getStatusCode(), var_export($document, TRUE));
|
||||
// Two users should have the second role. A sort is used on this request to
|
||||
// ensure a consistent ordering with different databases.
|
||||
$this->assertCount(2, $document['data']);
|
||||
$this->assertSame($users[1]->uuid(), $document['data'][0]['id']);
|
||||
$this->assertSame($users[2]->uuid(), $document['data'][1]['id']);
|
||||
|
||||
// Ensure that an entity can be filtered by an target entity integer ID.
|
||||
$response = $this->request('GET', Url::fromUri('internal:/jsonapi/node/article?filter[uid.meta.drupal_internal__target_id]=' . $users[1]->id()), $request_options);
|
||||
$document = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame(200, $response->getStatusCode(), var_export($document, TRUE));
|
||||
// Only the node authored by the filtered user should be returned.
|
||||
$this->assertCount(1, $document['data']);
|
||||
$this->assertSame('Article created by ' . $users[1]->uuid(), $document['data'][0]['attributes']['title']);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\node\NodeInterface;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "Date" field.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class JsonApiFunctionalDateFieldTest extends JsonApiFunctionalTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'basic_auth',
|
||||
'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
FieldStorageConfig::create([
|
||||
'field_name' => 'field_datetime',
|
||||
'entity_type' => 'node',
|
||||
'type' => 'datetime',
|
||||
'settings' => [
|
||||
'datetime_type' => 'datetime',
|
||||
],
|
||||
'cardinality' => 1,
|
||||
])->save();
|
||||
|
||||
FieldConfig::create([
|
||||
'field_name' => 'field_datetime',
|
||||
'label' => 'Date and time',
|
||||
'entity_type' => 'node',
|
||||
'bundle' => 'article',
|
||||
'required' => FALSE,
|
||||
'settings' => [],
|
||||
'description' => '',
|
||||
])->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the GET method.
|
||||
*/
|
||||
public function testRead(): void {
|
||||
/** @var \Drupal\Core\Datetime\DateFormatterInterface $date_formatter */
|
||||
$date_formatter = $this->container->get('date.formatter');
|
||||
|
||||
$timestamp_1 = 5000000;
|
||||
$timestamp_2 = 6000000;
|
||||
$timestamp_3 = 7000000;
|
||||
// Expected: node 1.
|
||||
$timestamp_smaller_than_value = $timestamp_2;
|
||||
// Expected: node 1 and node 2.
|
||||
$timestamp_smaller_than_or_equal_value = $timestamp_2;
|
||||
// Expected: node 3.
|
||||
$timestamp_greater_than_value = $timestamp_2;
|
||||
// Expected: node 2 and node 3.
|
||||
$timestamp_greater_than_or_equal_value = $timestamp_2;
|
||||
|
||||
$node_1 = $this->createNode([
|
||||
'type' => 'article',
|
||||
'uuid' => 'es_test_1',
|
||||
'status' => NodeInterface::PUBLISHED,
|
||||
'field_datetime' => $date_formatter->format($timestamp_1, 'custom', DateTimeItemInterface::DATETIME_STORAGE_FORMAT),
|
||||
]);
|
||||
$node_2 = $this->createNode([
|
||||
'type' => 'article',
|
||||
'uuid' => 'es_test_2',
|
||||
'status' => NodeInterface::PUBLISHED,
|
||||
'field_datetime' => $date_formatter->format($timestamp_2, 'custom', DateTimeItemInterface::DATETIME_STORAGE_FORMAT),
|
||||
]);
|
||||
$node_3 = $this->createNode([
|
||||
'type' => 'article',
|
||||
'uuid' => 'es_test_3',
|
||||
'status' => NodeInterface::PUBLISHED,
|
||||
'field_datetime' => $date_formatter->format($timestamp_3, 'custom', DateTimeItemInterface::DATETIME_STORAGE_FORMAT),
|
||||
]);
|
||||
|
||||
// Checks whether the date is greater than the given timestamp.
|
||||
$filter = [
|
||||
'filter_datetime' => [
|
||||
'condition' => [
|
||||
'path' => 'field_datetime',
|
||||
'operator' => '>',
|
||||
'value' => date(DateTimeItemInterface::DATETIME_STORAGE_FORMAT, $timestamp_greater_than_value),
|
||||
],
|
||||
],
|
||||
];
|
||||
$output = Json::decode($this->drupalGet('/jsonapi/node/article', [
|
||||
'query' => ['filter' => $filter],
|
||||
]));
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$output_uuids = array_map(function ($result) {
|
||||
return $result['id'];
|
||||
}, $output['data']);
|
||||
$this->assertCount(1, $output_uuids);
|
||||
$this->assertSame([
|
||||
$node_3->uuid(),
|
||||
], $output_uuids);
|
||||
|
||||
// Checks whether the date is greater than or equal to the given timestamp.
|
||||
$filter = [
|
||||
'filter_datetime' => [
|
||||
'condition' => [
|
||||
'path' => 'field_datetime',
|
||||
'operator' => '>=',
|
||||
'value' => date(DateTimeItemInterface::DATETIME_STORAGE_FORMAT, $timestamp_greater_than_or_equal_value),
|
||||
],
|
||||
],
|
||||
];
|
||||
$output = Json::decode($this->drupalGet('/jsonapi/node/article', [
|
||||
'query' => ['filter' => $filter],
|
||||
]));
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$output_uuids = array_map(function ($result) {
|
||||
return $result['id'];
|
||||
}, $output['data']);
|
||||
$this->assertCount(2, $output_uuids);
|
||||
$this->assertSame([
|
||||
$node_2->uuid(),
|
||||
$node_3->uuid(),
|
||||
], $output_uuids);
|
||||
|
||||
// Checks whether the date is less than the given timestamp.
|
||||
$filter = [
|
||||
'filter_datetime' => [
|
||||
'condition' => [
|
||||
'path' => 'field_datetime',
|
||||
'operator' => '<',
|
||||
'value' => date(DateTimeItemInterface::DATETIME_STORAGE_FORMAT, $timestamp_smaller_than_value),
|
||||
],
|
||||
],
|
||||
];
|
||||
$output = Json::decode($this->drupalGet('/jsonapi/node/article', [
|
||||
'query' => ['filter' => $filter],
|
||||
]));
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$output_uuids = array_map(function ($result) {
|
||||
return $result['id'];
|
||||
}, $output['data']);
|
||||
$this->assertCount(1, $output_uuids);
|
||||
$this->assertSame([
|
||||
$node_1->uuid(),
|
||||
], $output_uuids);
|
||||
|
||||
// Checks whether the date is less than or equal to the given timestamp.
|
||||
$filter = [
|
||||
'filter_datetime' => [
|
||||
'condition' => [
|
||||
'path' => 'field_datetime',
|
||||
'operator' => '<=',
|
||||
'value' => date(DateTimeItemInterface::DATETIME_STORAGE_FORMAT, $timestamp_smaller_than_or_equal_value),
|
||||
],
|
||||
],
|
||||
];
|
||||
$output = Json::decode($this->drupalGet('/jsonapi/node/article', [
|
||||
'query' => ['filter' => $filter],
|
||||
]));
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$output_uuids = array_map(function ($result) {
|
||||
return $result['id'];
|
||||
}, $output['data']);
|
||||
$this->assertCount(2, $output_uuids);
|
||||
$this->assertSame([
|
||||
$node_1->uuid(),
|
||||
$node_2->uuid(),
|
||||
], $output_uuids);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,327 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\language\Entity\ContentLanguageSettings;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\user\Entity\Role;
|
||||
use Drupal\user\RoleInterface;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
|
||||
/**
|
||||
* Tests JSON:API multilingual support.
|
||||
*
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class JsonApiFunctionalMultilingualTest extends JsonApiFunctionalTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'language',
|
||||
'content_translation',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$language = ConfigurableLanguage::createFromLangcode('ca');
|
||||
$language->save();
|
||||
ConfigurableLanguage::createFromLangcode('ca-fr')->save();
|
||||
|
||||
// In order to reflect the changes for a multilingual site in the container
|
||||
// we have to rebuild it.
|
||||
$this->rebuildContainer();
|
||||
|
||||
\Drupal::configFactory()->getEditable('language.negotiation')
|
||||
->set('url.prefixes.ca', 'ca')
|
||||
->set('url.prefixes.ca-fr', 'ca-fr')
|
||||
->save();
|
||||
|
||||
ContentLanguageSettings::create([
|
||||
'target_entity_type_id' => 'node',
|
||||
'target_bundle' => 'article',
|
||||
])
|
||||
->setThirdPartySetting('content_translation', 'enabled', TRUE)
|
||||
->save();
|
||||
|
||||
$this->createDefaultContent(5, 5, TRUE, TRUE, static::IS_MULTILINGUAL, FALSE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests reading multilingual content.
|
||||
*/
|
||||
public function testReadMultilingual(): void {
|
||||
// Different databases have different sort orders, so a sort is required so
|
||||
// test expectations do not need to vary per database.
|
||||
$default_sort = ['sort' => 'drupal_internal__nid'];
|
||||
|
||||
// Test reading an individual entity translation.
|
||||
$output = Json::decode($this->drupalGet('/ca/jsonapi/node/article/' . $this->nodes[0]->uuid(), ['query' => ['include' => 'field_tags,field_image'] + $default_sort]));
|
||||
$this->assertEquals($this->nodes[0]->getTranslation('ca')->getTitle(), $output['data']['attributes']['title']);
|
||||
$this->assertSame('ca', $output['data']['attributes']['langcode']);
|
||||
$included_tags = array_filter($output['included'], function ($entry) {
|
||||
return $entry['type'] === 'taxonomy_term--tags';
|
||||
});
|
||||
$tag_name = $this->nodes[0]->get('field_tags')->entity
|
||||
->getTranslation('ca')->getName();
|
||||
$this->assertEquals($tag_name, reset($included_tags)['attributes']['name']);
|
||||
$alt = $this->nodes[0]->getTranslation('ca')->get('field_image')->alt;
|
||||
$this->assertSame($alt, $output['data']['relationships']['field_image']['data']['meta']['alt']);
|
||||
|
||||
// Test reading an individual entity fallback.
|
||||
$output = Json::decode($this->drupalGet('/ca-fr/jsonapi/node/article/' . $this->nodes[0]->uuid()));
|
||||
$this->assertEquals($this->nodes[0]->getTranslation('ca')->getTitle(), $output['data']['attributes']['title']);
|
||||
|
||||
$output = Json::decode($this->drupalGet('/ca/jsonapi/node/article/' . $this->nodes[0]->uuid(), ['query' => $default_sort]));
|
||||
$this->assertEquals($this->nodes[0]->getTranslation('ca')->getTitle(), $output['data']['attributes']['title']);
|
||||
|
||||
// Test reading a collection of entities.
|
||||
$output = Json::decode($this->drupalGet('/ca/jsonapi/node/article', ['query' => $default_sort]));
|
||||
$this->assertEquals($this->nodes[0]->getTranslation('ca')->getTitle(), $output['data'][0]['attributes']['title']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests updating a translation.
|
||||
*/
|
||||
public function testPatchTranslation(): void {
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
$node = $this->nodes[0];
|
||||
$uuid = $node->uuid();
|
||||
|
||||
// Assert the precondition: the 'ca' translation has a different title.
|
||||
$document = Json::decode($this->drupalGet('/jsonapi/node/article/' . $uuid));
|
||||
$document_ca = Json::decode($this->drupalGet('/ca/jsonapi/node/article/' . $uuid));
|
||||
$this->assertSame('en', $document['data']['attributes']['langcode']);
|
||||
$this->assertSame('ca', $document_ca['data']['attributes']['langcode']);
|
||||
$this->assertSame($node->getTitle(), $document['data']['attributes']['title']);
|
||||
$this->assertSame($node->getTitle() . ' (ca)', $document_ca['data']['attributes']['title']);
|
||||
|
||||
// PATCH the 'ca' translation.
|
||||
$this->grantPermissions(Role::load(RoleInterface::ANONYMOUS_ID), [
|
||||
'bypass node access',
|
||||
]);
|
||||
$request_options = [];
|
||||
$request_options[RequestOptions::HEADERS]['Content-Type'] = 'application/vnd.api+json';
|
||||
$request_options[RequestOptions::BODY] = Json::encode([
|
||||
'data' => [
|
||||
'type' => 'node--article',
|
||||
'id' => $uuid,
|
||||
'attributes' => [
|
||||
'title' => $document_ca['data']['attributes']['title'] . ' UPDATED',
|
||||
],
|
||||
],
|
||||
]);
|
||||
$response = $this->request('PATCH', Url::fromUri('base:/ca/jsonapi/node/article/' . $this->nodes[0]->uuid()), $request_options);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
|
||||
// Assert the postcondition: only the 'ca' translation has an updated title.
|
||||
$document_updated = Json::decode($this->drupalGet('/jsonapi/node/article/' . $uuid));
|
||||
$document_ca_updated = Json::decode($this->drupalGet('/ca/jsonapi/node/article/' . $uuid));
|
||||
$this->assertSame('en', $document_updated['data']['attributes']['langcode']);
|
||||
$this->assertSame('ca', $document_ca_updated['data']['attributes']['langcode']);
|
||||
$this->assertSame($node->getTitle(), $document_updated['data']['attributes']['title']);
|
||||
$this->assertSame($node->getTitle() . ' (ca) UPDATED', $document_ca_updated['data']['attributes']['title']);
|
||||
|
||||
// Specifying a langcode is not allowed by default.
|
||||
$request_options[RequestOptions::BODY] = Json::encode([
|
||||
'data' => [
|
||||
'type' => 'node--article',
|
||||
'id' => $uuid,
|
||||
'attributes' => [
|
||||
'langcode' => 'ca-fr',
|
||||
],
|
||||
],
|
||||
]);
|
||||
$response = $this->request('PATCH', Url::fromUri('base:/ca/jsonapi/node/article/' . $this->nodes[0]->uuid()), $request_options);
|
||||
$this->assertSame(403, $response->getStatusCode());
|
||||
|
||||
// Specifying a langcode is allowed once configured to be alterable. But
|
||||
// modifying the language of a non-default translation is still not allowed.
|
||||
ContentLanguageSettings::loadByEntityTypeBundle('node', 'article')
|
||||
->setLanguageAlterable(TRUE)
|
||||
->save();
|
||||
$response = $this->request('PATCH', Url::fromUri('base:/ca/jsonapi/node/article/' . $this->nodes[0]->uuid()), $request_options);
|
||||
$this->assertSame(500, $response->getStatusCode());
|
||||
$document = $this->getDocumentFromResponse($response, FALSE);
|
||||
$this->assertSame('The translation language cannot be changed (ca).', $document['errors'][0]['detail']);
|
||||
|
||||
// Changing the langcode of the default ('en') translation is possible:
|
||||
// first verify that it currently is 'en', then change it to 'ca-fr', and
|
||||
// verify that the title is unchanged, but the langcode is updated.
|
||||
$response = $this->request('GET', Url::fromUri('base:/jsonapi/node/article/' . $this->nodes[0]->uuid()), $request_options);
|
||||
$document = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
$this->assertSame($node->getTitle(), $document['data']['attributes']['title']);
|
||||
$this->assertSame('en', $document['data']['attributes']['langcode']);
|
||||
$response = $this->request('PATCH', Url::fromUri('base:/jsonapi/node/article/' . $this->nodes[0]->uuid()), $request_options);
|
||||
$document = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
$this->assertSame($node->getTitle(), $document['data']['attributes']['title']);
|
||||
$this->assertSame('ca-fr', $document['data']['attributes']['langcode']);
|
||||
|
||||
// Finally: assert the postcondition of all installed languages.
|
||||
// - When GETting the 'en' translation, we get 'ca-fr', since the 'en'
|
||||
// translation doesn't exist anymore.
|
||||
$response = $this->request('GET', Url::fromUri('base:/jsonapi/node/article/' . $this->nodes[0]->uuid()), $request_options);
|
||||
$document = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame('ca-fr', $document['data']['attributes']['langcode']);
|
||||
$this->assertSame($node->getTitle(), $document['data']['attributes']['title']);
|
||||
// - When GETting the 'ca' translation, we still get the 'ca' one.
|
||||
$response = $this->request('GET', Url::fromUri('base:/ca/jsonapi/node/article/' . $this->nodes[0]->uuid()), $request_options);
|
||||
$document = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame('ca', $document['data']['attributes']['langcode']);
|
||||
$this->assertSame($node->getTitle() . ' (ca) UPDATED', $document['data']['attributes']['title']);
|
||||
// - When GETting the 'ca-fr' translation, we now get the default
|
||||
// translation.
|
||||
$response = $this->request('GET', Url::fromUri('base:/ca-fr/jsonapi/node/article/' . $this->nodes[0]->uuid()), $request_options);
|
||||
$document = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame('ca-fr', $document['data']['attributes']['langcode']);
|
||||
$this->assertSame($node->getTitle(), $document['data']['attributes']['title']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests updating a translation fallback.
|
||||
*/
|
||||
public function testPatchTranslationFallback(): void {
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
$node = $this->nodes[0];
|
||||
$uuid = $node->uuid();
|
||||
|
||||
// Assert the precondition: 'ca-fr' falls back to the 'ca' translation which
|
||||
// has a different title.
|
||||
$document = Json::decode($this->drupalGet('/jsonapi/node/article/' . $uuid));
|
||||
$document_ca = Json::decode($this->drupalGet('/ca/jsonapi/node/article/' . $uuid));
|
||||
$document_ca_fr = Json::decode($this->drupalGet('/ca-fr/jsonapi/node/article/' . $uuid));
|
||||
$this->assertSame('en', $document['data']['attributes']['langcode']);
|
||||
$this->assertSame('ca', $document_ca['data']['attributes']['langcode']);
|
||||
$this->assertSame('ca', $document_ca_fr['data']['attributes']['langcode']);
|
||||
$this->assertSame($node->getTitle(), $document['data']['attributes']['title']);
|
||||
$this->assertSame($node->getTitle() . ' (ca)', $document_ca['data']['attributes']['title']);
|
||||
$this->assertSame($node->getTitle() . ' (ca)', $document_ca_fr['data']['attributes']['title']);
|
||||
|
||||
// PATCH the 'ca-fr' translation.
|
||||
$this->grantPermissions(Role::load(RoleInterface::ANONYMOUS_ID), [
|
||||
'bypass node access',
|
||||
]);
|
||||
$request_options = [];
|
||||
$request_options[RequestOptions::HEADERS]['Content-Type'] = 'application/vnd.api+json';
|
||||
$request_options[RequestOptions::BODY] = Json::encode([
|
||||
'data' => [
|
||||
'type' => 'node--article',
|
||||
'id' => $uuid,
|
||||
'attributes' => [
|
||||
'title' => $document_ca_fr['data']['attributes']['title'] . ' UPDATED',
|
||||
],
|
||||
],
|
||||
]);
|
||||
$response = $this->request('PATCH', Url::fromUri('base:/ca-fr/jsonapi/node/article/' . $this->nodes[0]->uuid()), $request_options);
|
||||
$this->assertSame(405, $response->getStatusCode());
|
||||
$document = $this->getDocumentFromResponse($response, FALSE);
|
||||
$this->assertSame('The requested translation of the resource object does not exist, instead modify one of the translations that do exist: ca, en.', $document['errors'][0]['detail']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests creating a translation.
|
||||
*/
|
||||
public function testPostTranslation(): void {
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
$this->grantPermissions(Role::load(RoleInterface::ANONYMOUS_ID), [
|
||||
'bypass node access',
|
||||
]);
|
||||
|
||||
$title = 'Llamas FTW (ca)';
|
||||
$request_document = [
|
||||
'data' => [
|
||||
'type' => 'node--article',
|
||||
'attributes' => [
|
||||
'title' => $title,
|
||||
'langcode' => 'ca',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$request_options = [];
|
||||
$request_options[RequestOptions::HEADERS]['Content-Type'] = 'application/vnd.api+json';
|
||||
|
||||
// Specifying a langcode is forbidden by language_entity_field_access().
|
||||
$request_options[RequestOptions::BODY] = Json::encode($request_document);
|
||||
$response = $this->request('POST', Url::fromUri('base:/ca/jsonapi/node/article/'), $request_options);
|
||||
$this->assertSame(403, $response->getStatusCode());
|
||||
$document = $this->getDocumentFromResponse($response, FALSE);
|
||||
$this->assertSame('The current user is not allowed to POST the selected field (langcode).', $document['errors'][0]['detail']);
|
||||
|
||||
// Omitting a langcode results in an entity in 'en': the default language of
|
||||
// the site.
|
||||
unset($request_document['data']['attributes']['langcode']);
|
||||
$request_options[RequestOptions::BODY] = Json::encode($request_document);
|
||||
$response = $this->request('POST', Url::fromUri('base:/ca/jsonapi/node/article/'), $request_options);
|
||||
$document = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame(201, $response->getStatusCode());
|
||||
$this->assertSame($title, $document['data']['attributes']['title']);
|
||||
$this->assertSame('en', $document['data']['attributes']['langcode']);
|
||||
$this->assertSame(['en'], array_keys(Node::load($document['data']['attributes']['drupal_internal__nid'])->getTranslationLanguages()));
|
||||
|
||||
// Specifying a langcode is allowed once configured to be alterable. Now an
|
||||
// entity can be created with the specified langcode.
|
||||
ContentLanguageSettings::loadByEntityTypeBundle('node', 'article')
|
||||
->setLanguageAlterable(TRUE)
|
||||
->save();
|
||||
$request_document['data']['attributes']['langcode'] = 'ca';
|
||||
$request_options[RequestOptions::BODY] = Json::encode($request_document);
|
||||
$response = $this->request('POST', Url::fromUri('base:/ca/jsonapi/node/article/'), $request_options);
|
||||
$document = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame(201, $response->getStatusCode());
|
||||
$this->assertSame($title, $document['data']['attributes']['title']);
|
||||
$this->assertSame('ca', $document['data']['attributes']['langcode']);
|
||||
$this->assertSame(['ca'], array_keys(Node::load($document['data']['attributes']['drupal_internal__nid'])->getTranslationLanguages()));
|
||||
|
||||
// Same request, but sent to the URL without the language prefix.
|
||||
$response = $this->request('POST', Url::fromUri('base:/jsonapi/node/article/'), $request_options);
|
||||
$document = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame(201, $response->getStatusCode());
|
||||
$this->assertSame($title, $document['data']['attributes']['title']);
|
||||
$this->assertSame('ca', $document['data']['attributes']['langcode']);
|
||||
$this->assertSame(['ca'], array_keys(Node::load($document['data']['attributes']['drupal_internal__nid'])->getTranslationLanguages()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests deleting multilingual content.
|
||||
*/
|
||||
public function testDeleteMultilingual(): void {
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
$this->grantPermissions(Role::load(RoleInterface::ANONYMOUS_ID), [
|
||||
'bypass node access',
|
||||
]);
|
||||
|
||||
$response = $this->request('DELETE', Url::fromUri('base:/ca/jsonapi/node/article/' . $this->nodes[0]->uuid()), []);
|
||||
$this->assertSame(405, $response->getStatusCode());
|
||||
$document = $this->getDocumentFromResponse($response, FALSE);
|
||||
$this->assertSame('Deleting a resource object translation is not yet supported. See https://www.drupal.org/docs/8/modules/jsonapi/translations.', $document['errors'][0]['detail']);
|
||||
|
||||
$response = $this->request('DELETE', Url::fromUri('base:/ca-fr/jsonapi/node/article/' . $this->nodes[0]->uuid()), []);
|
||||
$this->assertSame(405, $response->getStatusCode());
|
||||
$document = $this->getDocumentFromResponse($response, FALSE);
|
||||
$this->assertSame('Deleting a resource object translation is not yet supported. See https://www.drupal.org/docs/8/modules/jsonapi/translations.', $document['errors'][0]['detail']);
|
||||
|
||||
$response = $this->request('DELETE', Url::fromUri('base:/jsonapi/node/article/' . $this->nodes[0]->uuid()), []);
|
||||
$this->assertSame(204, $response->getStatusCode());
|
||||
$this->assertNull(Node::load($this->nodes[0]->id()));
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,356 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\file\Entity\File;
|
||||
use Drupal\taxonomy\Entity\Term;
|
||||
use Drupal\taxonomy\Entity\Vocabulary;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
|
||||
use Drupal\Tests\image\Kernel\ImageFieldCreationTrait;
|
||||
use Drupal\Tests\jsonapi\Traits\GetDocumentFromResponseTrait;
|
||||
use Drupal\user\Entity\Role;
|
||||
use Drupal\user\RoleInterface;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use GuzzleHttp\Exception\ServerException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Provides helper methods for the JSON:API module's functional tests.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
abstract class JsonApiFunctionalTestBase extends BrowserTestBase {
|
||||
|
||||
use EntityReferenceFieldCreationTrait;
|
||||
use GetDocumentFromResponseTrait;
|
||||
use ImageFieldCreationTrait;
|
||||
|
||||
const IS_MULTILINGUAL = TRUE;
|
||||
const IS_NOT_MULTILINGUAL = FALSE;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'jsonapi',
|
||||
'serialization',
|
||||
'node',
|
||||
'image',
|
||||
'taxonomy',
|
||||
'link',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* Test user.
|
||||
*
|
||||
* @var \Drupal\user\Entity\User
|
||||
*/
|
||||
protected $user;
|
||||
|
||||
/**
|
||||
* Test admin user.
|
||||
*
|
||||
* @var \Drupal\user\Entity\User
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* Test user with access to view profiles.
|
||||
*
|
||||
* @var \Drupal\user\Entity\User
|
||||
*/
|
||||
protected $userCanViewProfiles;
|
||||
|
||||
/**
|
||||
* Test nodes.
|
||||
*
|
||||
* @var \Drupal\node\Entity\Node[]
|
||||
*/
|
||||
protected $nodes = [];
|
||||
|
||||
/**
|
||||
* Test taxonomy terms.
|
||||
*
|
||||
* @var \Drupal\taxonomy\Entity\Term[]
|
||||
*/
|
||||
protected $tags = [];
|
||||
|
||||
/**
|
||||
* Test files.
|
||||
*
|
||||
* @var \Drupal\file\Entity\File[]
|
||||
*/
|
||||
protected $files = [];
|
||||
|
||||
/**
|
||||
* The HTTP client.
|
||||
*
|
||||
* @var \GuzzleHttp\ClientInterface
|
||||
*/
|
||||
protected $httpClient;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
// Set up a HTTP client that accepts relative URLs.
|
||||
$this->httpClient = $this->container->get('http_client_factory')
|
||||
->fromOptions(['base_uri' => $this->baseUrl]);
|
||||
|
||||
// Create Basic page and Article node types.
|
||||
if ($this->profile != 'standard') {
|
||||
$this->drupalCreateContentType([
|
||||
'type' => 'article',
|
||||
'name' => 'Article',
|
||||
]);
|
||||
|
||||
// Setup vocabulary.
|
||||
Vocabulary::create([
|
||||
'vid' => 'tags',
|
||||
'name' => 'Tags',
|
||||
])->save();
|
||||
|
||||
// Add tags and field_image to the article.
|
||||
$this->createEntityReferenceField(
|
||||
'node',
|
||||
'article',
|
||||
'field_tags',
|
||||
'Tags',
|
||||
'taxonomy_term',
|
||||
'default',
|
||||
[
|
||||
'target_bundles' => [
|
||||
'tags' => 'tags',
|
||||
],
|
||||
'auto_create' => TRUE,
|
||||
],
|
||||
FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
|
||||
);
|
||||
$this->createImageField('field_image', 'node', 'article');
|
||||
$this->createImageField('field_no_hero', 'node', 'article');
|
||||
}
|
||||
|
||||
FieldStorageConfig::create([
|
||||
'field_name' => 'field_link',
|
||||
'entity_type' => 'node',
|
||||
'type' => 'link',
|
||||
'settings' => [],
|
||||
'cardinality' => 1,
|
||||
])->save();
|
||||
|
||||
$field_config = FieldConfig::create([
|
||||
'field_name' => 'field_link',
|
||||
'label' => 'Link',
|
||||
'entity_type' => 'node',
|
||||
'bundle' => 'article',
|
||||
'required' => FALSE,
|
||||
'settings' => [],
|
||||
'description' => '',
|
||||
]);
|
||||
$field_config->save();
|
||||
|
||||
// Field for testing sorting.
|
||||
FieldStorageConfig::create([
|
||||
'field_name' => 'field_sort1',
|
||||
'entity_type' => 'node',
|
||||
'type' => 'integer',
|
||||
])->save();
|
||||
FieldConfig::create([
|
||||
'field_name' => 'field_sort1',
|
||||
'entity_type' => 'node',
|
||||
'bundle' => 'article',
|
||||
])->save();
|
||||
|
||||
// Another field for testing sorting.
|
||||
FieldStorageConfig::create([
|
||||
'field_name' => 'field_sort2',
|
||||
'entity_type' => 'node',
|
||||
'type' => 'integer',
|
||||
])->save();
|
||||
FieldConfig::create([
|
||||
'field_name' => 'field_sort2',
|
||||
'entity_type' => 'node',
|
||||
'bundle' => 'article',
|
||||
])->save();
|
||||
|
||||
$this->user = $this->drupalCreateUser([
|
||||
'create article content',
|
||||
'edit any article content',
|
||||
'delete any article content',
|
||||
]);
|
||||
$this->adminUser = $this->drupalCreateUser([
|
||||
'create article content',
|
||||
'edit any article content',
|
||||
'delete any article content',
|
||||
],
|
||||
'jsonapi_admin_user',
|
||||
TRUE,
|
||||
);
|
||||
|
||||
// Create a user that can.
|
||||
$this->userCanViewProfiles = $this->drupalCreateUser([
|
||||
'access user profiles',
|
||||
]);
|
||||
|
||||
$this->grantPermissions(Role::load(RoleInterface::ANONYMOUS_ID), [
|
||||
'access user profiles',
|
||||
'administer taxonomy',
|
||||
]);
|
||||
|
||||
\Drupal::service('router.builder')->rebuild();
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a HTTP request. Wraps the Guzzle HTTP client.
|
||||
*
|
||||
* Why wrap the Guzzle HTTP client? Because any error response is returned via
|
||||
* an exception, which would make the tests unnecessarily complex to read.
|
||||
*
|
||||
* @param string $method
|
||||
* HTTP method.
|
||||
* @param \Drupal\Core\Url $url
|
||||
* URL to request.
|
||||
* @param array $request_options
|
||||
* Request options to apply.
|
||||
*
|
||||
* @return \Psr\Http\Message\ResponseInterface
|
||||
* The request response.
|
||||
*
|
||||
* @throws \Psr\Http\Client\ClientExceptionInterface
|
||||
*
|
||||
* @see \GuzzleHttp\ClientInterface::request
|
||||
*/
|
||||
protected function request($method, Url $url, array $request_options): ResponseInterface {
|
||||
try {
|
||||
$response = $this->httpClient->request($method, $url->toString(), $request_options);
|
||||
}
|
||||
catch (ClientException $e) {
|
||||
$response = $e->getResponse();
|
||||
}
|
||||
catch (ServerException $e) {
|
||||
$response = $e->getResponse();
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates default content to test the API.
|
||||
*
|
||||
* @param int $num_articles
|
||||
* Number of articles to create.
|
||||
* @param int $num_tags
|
||||
* Number of tags to create.
|
||||
* @param bool $article_has_image
|
||||
* Set to TRUE if you want to add an image to the generated articles.
|
||||
* @param bool $article_has_link
|
||||
* Set to TRUE if you want to add a link to the generated articles.
|
||||
* @param bool $is_multilingual
|
||||
* (optional) Set to TRUE if you want to enable multilingual content.
|
||||
* @param bool $referencing_twice
|
||||
* (optional) Set to TRUE if you want articles to reference the same tag
|
||||
* twice.
|
||||
*/
|
||||
protected function createDefaultContent($num_articles, $num_tags, $article_has_image, $article_has_link, $is_multilingual, $referencing_twice = FALSE) {
|
||||
$random = $this->getRandomGenerator();
|
||||
for ($created_tags = 0; $created_tags < $num_tags; $created_tags++) {
|
||||
$term = Term::create([
|
||||
'vid' => 'tags',
|
||||
'name' => $random->name(),
|
||||
]);
|
||||
|
||||
if ($is_multilingual) {
|
||||
$term->addTranslation('ca', ['name' => $term->getName() . ' (ca)']);
|
||||
}
|
||||
|
||||
$term->save();
|
||||
$this->tags[] = $term;
|
||||
}
|
||||
for ($created_nodes = 0; $created_nodes < $num_articles; $created_nodes++) {
|
||||
$values = [
|
||||
'uid' => ['target_id' => $this->user->id()],
|
||||
'type' => 'article',
|
||||
];
|
||||
|
||||
if ($referencing_twice) {
|
||||
$values['field_tags'] = [
|
||||
['target_id' => 1],
|
||||
['target_id' => 1],
|
||||
];
|
||||
}
|
||||
else {
|
||||
// Get N random tags.
|
||||
$selected_tags = mt_rand(1, $num_tags);
|
||||
$tags = [];
|
||||
while (count($tags) < $selected_tags) {
|
||||
$tags[] = mt_rand(1, $num_tags);
|
||||
$tags = array_unique($tags);
|
||||
}
|
||||
$values['field_tags'] = array_map(function ($tag) {
|
||||
return ['target_id' => $tag];
|
||||
}, $tags);
|
||||
}
|
||||
if ($article_has_image) {
|
||||
$file = File::create([
|
||||
'uri' => 'public://' . $random->name() . '.png',
|
||||
]);
|
||||
$file->setPermanent();
|
||||
$file->save();
|
||||
$this->files[] = $file;
|
||||
$values['field_image'] = ['target_id' => $file->id(), 'alt' => 'alt text'];
|
||||
}
|
||||
if ($article_has_link) {
|
||||
$values['field_link'] = [
|
||||
'title' => $this->getRandomGenerator()->name(),
|
||||
'uri' => sprintf(
|
||||
'%s://%s.%s',
|
||||
'http' . (mt_rand(0, 2) > 1 ? '' : 's'),
|
||||
$this->getRandomGenerator()->name(),
|
||||
'org'
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
// Create values for the sort fields, to allow for testing complex
|
||||
// sorting:
|
||||
// - field_sort1 increments every 5 articles, starting at zero
|
||||
// - field_sort2 decreases every article, ending at zero.
|
||||
$values['field_sort1'] = ['value' => floor($created_nodes / 5)];
|
||||
$values['field_sort2'] = ['value' => $num_articles - $created_nodes];
|
||||
|
||||
$node = $this->createNode($values);
|
||||
|
||||
if ($is_multilingual === static::IS_MULTILINGUAL) {
|
||||
$values['title'] = $node->getTitle() . ' (ca)';
|
||||
$values['field_image']['alt'] = 'alt text (ca)';
|
||||
$node->addTranslation('ca', $values);
|
||||
}
|
||||
$node->save();
|
||||
|
||||
$this->nodes[] = $node;
|
||||
}
|
||||
if ($article_has_link) {
|
||||
// Make sure that there is at least 1 https link for ::testRead() #19.
|
||||
$this->nodes[0]->field_link = [
|
||||
'title' => 'Drupal',
|
||||
'uri' => 'https://example.com',
|
||||
];
|
||||
$this->nodes[0]->save();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,491 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\comment\Entity\Comment;
|
||||
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
|
||||
use Drupal\comment\Tests\CommentTestTrait;
|
||||
use Drupal\Core\Entity\TranslatableInterface;
|
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
|
||||
use Drupal\entity_test\Entity\EntityTest;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\taxonomy\Entity\Term;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
|
||||
/**
|
||||
* JSON:API regression tests.
|
||||
*
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class JsonApiPatchRegressionTest extends JsonApiFunctionalTestBase {
|
||||
|
||||
use CommentTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'basic_auth',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* Ensure filtering on relationships works with bundle-specific target types.
|
||||
*
|
||||
* @see https://www.drupal.org/project/drupal/issues/2953207
|
||||
*/
|
||||
public function testBundleSpecificTargetEntityTypeFromIssue2953207(): void {
|
||||
// Set up data model.
|
||||
$this->assertTrue($this->container->get('module_installer')->install(['comment'], TRUE), 'Installed modules.');
|
||||
$this->addDefaultCommentField('taxonomy_term', 'tags', 'comment', CommentItemInterface::OPEN, 'test_comment_type');
|
||||
$this->rebuildAll();
|
||||
|
||||
// Create data.
|
||||
Term::create([
|
||||
'name' => 'foobar',
|
||||
'vid' => 'tags',
|
||||
])->save();
|
||||
Comment::create([
|
||||
'subject' => 'Llama',
|
||||
'entity_id' => 1,
|
||||
'entity_type' => 'taxonomy_term',
|
||||
'field_name' => 'comment',
|
||||
])->save();
|
||||
|
||||
// Test.
|
||||
$user = $this->drupalCreateUser([
|
||||
'access comments',
|
||||
]);
|
||||
$response = $this->request('GET', Url::fromUri('internal:/jsonapi/comment/test_comment_type?include=entity_id&filter[entity_id.name]=foobar'), [
|
||||
RequestOptions::AUTH => [
|
||||
$user->getAccountName(),
|
||||
$user->pass_raw,
|
||||
],
|
||||
]);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure POST and PATCH works for bundle-less relationship routes.
|
||||
*
|
||||
* @see https://www.drupal.org/project/drupal/issues/2976371
|
||||
*/
|
||||
public function testBundlelessRelationshipMutationFromIssue2973681(): void {
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
|
||||
// Set up data model.
|
||||
$this->drupalCreateContentType(['type' => 'page']);
|
||||
$this->createEntityReferenceField(
|
||||
'node',
|
||||
'page',
|
||||
'field_test',
|
||||
NULL,
|
||||
'user',
|
||||
'default',
|
||||
[
|
||||
'target_bundles' => NULL,
|
||||
],
|
||||
FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
|
||||
);
|
||||
$this->rebuildAll();
|
||||
|
||||
// Create data.
|
||||
$node = Node::create([
|
||||
'title' => 'test article',
|
||||
'type' => 'page',
|
||||
]);
|
||||
$node->save();
|
||||
$target = $this->createUser();
|
||||
|
||||
// Test.
|
||||
$user = $this->drupalCreateUser(['bypass node access']);
|
||||
$url = Url::fromRoute('jsonapi.node--page.field_test.relationship.post', ['entity' => $node->uuid()]);
|
||||
$request_options = [
|
||||
RequestOptions::HEADERS => [
|
||||
'Content-Type' => 'application/vnd.api+json',
|
||||
'Accept' => 'application/vnd.api+json',
|
||||
],
|
||||
RequestOptions::AUTH => [$user->getAccountName(), $user->pass_raw],
|
||||
RequestOptions::JSON => [
|
||||
'data' => [
|
||||
['type' => 'user--user', 'id' => $target->uuid()],
|
||||
],
|
||||
],
|
||||
];
|
||||
$response = $this->request('POST', $url, $request_options);
|
||||
$this->assertSame(204, $response->getStatusCode(), (string) $response->getBody());
|
||||
}
|
||||
|
||||
/**
|
||||
* Cannot PATCH an entity with dangling references in an ER field.
|
||||
*
|
||||
* @see https://www.drupal.org/project/drupal/issues/2968972
|
||||
*/
|
||||
public function testDanglingReferencesInAnEntityReferenceFieldFromIssue2968972(): void {
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
|
||||
// Set up data model.
|
||||
$this->drupalCreateContentType(['type' => 'journal_issue']);
|
||||
$this->drupalCreateContentType(['type' => 'journal_article']);
|
||||
$this->createEntityReferenceField(
|
||||
'node',
|
||||
'journal_article',
|
||||
'field_issue',
|
||||
NULL,
|
||||
'node',
|
||||
'default',
|
||||
[
|
||||
'target_bundles' => [
|
||||
'journal_issue' => 'journal_issue',
|
||||
],
|
||||
],
|
||||
FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
|
||||
);
|
||||
$this->rebuildAll();
|
||||
|
||||
// Create data.
|
||||
$issue_node = Node::create([
|
||||
'title' => 'Test Journal Issue',
|
||||
'type' => 'journal_issue',
|
||||
]);
|
||||
$issue_node->save();
|
||||
|
||||
$user = $this->drupalCreateUser([
|
||||
'access content',
|
||||
'edit own journal_article content',
|
||||
]);
|
||||
$article_node = Node::create([
|
||||
'title' => 'Test Journal Article',
|
||||
'type' => 'journal_article',
|
||||
'field_issue' => [
|
||||
'target_id' => $issue_node->id(),
|
||||
],
|
||||
]);
|
||||
$article_node->setOwner($user);
|
||||
$article_node->save();
|
||||
|
||||
// Test.
|
||||
$url = Url::fromUri(sprintf('internal:/jsonapi/node/journal_article/%s', $article_node->uuid()));
|
||||
$request_options = [
|
||||
RequestOptions::HEADERS => [
|
||||
'Content-Type' => 'application/vnd.api+json',
|
||||
'Accept' => 'application/vnd.api+json',
|
||||
],
|
||||
RequestOptions::AUTH => [$user->getAccountName(), $user->pass_raw],
|
||||
RequestOptions::JSON => [
|
||||
'data' => [
|
||||
'type' => 'node--journal_article',
|
||||
'id' => $article_node->uuid(),
|
||||
'attributes' => [
|
||||
'title' => 'My New Article Title',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
$issue_node->delete();
|
||||
$response = $this->request('PATCH', $url, $request_options);
|
||||
$this->assertSame(200, $response->getStatusCode(), (string) $response->getBody());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures PATCHing datetime (both date-only & date+time) fields is possible.
|
||||
*
|
||||
* @see https://www.drupal.org/project/drupal/issues/3021194
|
||||
*/
|
||||
public function testPatchingDateTimeFieldsFromIssue3021194(): void {
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
|
||||
// Set up data model.
|
||||
$this->assertTrue($this->container->get('module_installer')->install(['datetime'], TRUE), 'Installed modules.');
|
||||
$this->drupalCreateContentType(['type' => 'page']);
|
||||
$this->rebuildAll();
|
||||
FieldStorageConfig::create([
|
||||
'field_name' => 'when',
|
||||
'type' => 'datetime',
|
||||
'entity_type' => 'node',
|
||||
'settings' => ['datetime_type' => DateTimeItem::DATETIME_TYPE_DATE],
|
||||
])
|
||||
->save();
|
||||
FieldConfig::create([
|
||||
'field_name' => 'when',
|
||||
'entity_type' => 'node',
|
||||
'bundle' => 'page',
|
||||
])
|
||||
->save();
|
||||
FieldStorageConfig::create([
|
||||
'field_name' => 'when_exactly',
|
||||
'type' => 'datetime',
|
||||
'entity_type' => 'node',
|
||||
'settings' => ['datetime_type' => DateTimeItem::DATETIME_TYPE_DATETIME],
|
||||
])
|
||||
->save();
|
||||
FieldConfig::create([
|
||||
'field_name' => 'when_exactly',
|
||||
'entity_type' => 'node',
|
||||
'bundle' => 'page',
|
||||
])
|
||||
->save();
|
||||
|
||||
// Create data.
|
||||
$page = Node::create([
|
||||
'title' => 'Stegosaurus',
|
||||
'type' => 'page',
|
||||
'when' => [
|
||||
'value' => '2018-12-19',
|
||||
],
|
||||
'when_exactly' => [
|
||||
'value' => '2018-12-19T17:00:00',
|
||||
],
|
||||
]);
|
||||
$page->save();
|
||||
|
||||
// Test.
|
||||
$user = $this->drupalCreateUser([
|
||||
'access content',
|
||||
'edit any page content',
|
||||
]);
|
||||
$request_options = [
|
||||
RequestOptions::AUTH => [
|
||||
$user->getAccountName(),
|
||||
$user->pass_raw,
|
||||
],
|
||||
RequestOptions::HEADERS => [
|
||||
'Content-Type' => 'application/vnd.api+json',
|
||||
'Accept' => 'application/vnd.api+json',
|
||||
],
|
||||
];
|
||||
$node_url = Url::fromUri('internal:/jsonapi/node/page/' . $page->uuid());
|
||||
$response = $this->request('GET', $node_url, $request_options);
|
||||
$document = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
$this->assertSame('2018-12-19', $document['data']['attributes']['when']);
|
||||
$this->assertSame('2018-12-20T04:00:00+11:00', $document['data']['attributes']['when_exactly']);
|
||||
$document['data']['attributes']['when'] = '2018-12-20';
|
||||
$document['data']['attributes']['when_exactly'] = '2018-12-19T19:00:00+01:00';
|
||||
$request_options = $request_options + [RequestOptions::JSON => $document];
|
||||
$response = $this->request('PATCH', $node_url, $request_options);
|
||||
$document = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
$this->assertSame('2018-12-20', $document['data']['attributes']['when']);
|
||||
$this->assertSame('2018-12-20T05:00:00+11:00', $document['data']['attributes']['when_exactly']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure includes are respected even when PATCHing.
|
||||
*
|
||||
* @see https://www.drupal.org/project/drupal/issues/3026030
|
||||
*/
|
||||
public function testPatchToIncludeUrlDoesNotReturnIncludeFromIssue3026030(): void {
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
|
||||
// Set up data model.
|
||||
$this->drupalCreateContentType(['type' => 'page']);
|
||||
$this->rebuildAll();
|
||||
|
||||
// Create data.
|
||||
$user = $this->drupalCreateUser(['bypass node access']);
|
||||
$page = Node::create([
|
||||
'title' => 'original',
|
||||
'type' => 'page',
|
||||
'uid' => $user->id(),
|
||||
]);
|
||||
$page->save();
|
||||
|
||||
// Test.
|
||||
$url = Url::fromUri(sprintf('internal:/jsonapi/node/page/%s/?include=uid', $page->uuid()));
|
||||
$request_options = [
|
||||
RequestOptions::HEADERS => [
|
||||
'Content-Type' => 'application/vnd.api+json',
|
||||
'Accept' => 'application/vnd.api+json',
|
||||
],
|
||||
RequestOptions::AUTH => [$user->getAccountName(), $user->pass_raw],
|
||||
RequestOptions::JSON => [
|
||||
'data' => [
|
||||
'type' => 'node--page',
|
||||
'id' => $page->uuid(),
|
||||
'attributes' => [
|
||||
'title' => 'modified',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
$response = $this->request('PATCH', $url, $request_options);
|
||||
$document = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
$this->assertArrayHasKey('included', $document);
|
||||
$this->assertSame($user->label(), $document['included'][0]['attributes']['name']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure non-translatable entities can be PATCHed with an alternate language.
|
||||
*
|
||||
* @see https://www.drupal.org/project/drupal/issues/3043168
|
||||
*/
|
||||
public function testNonTranslatableEntityUpdatesFromIssue3043168(): void {
|
||||
// Enable write-mode.
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
// Set the site language to Russian.
|
||||
$this->config('system.site')->set('langcode', 'ru')->set('default_langcode', 'ru')->save(TRUE);
|
||||
// Install a "custom" entity type that is not translatable.
|
||||
$this->assertTrue($this->container->get('module_installer')->install(['entity_test'], TRUE), 'Installed modules.');
|
||||
// Clear and rebuild caches and routes.
|
||||
$this->rebuildAll();
|
||||
// Create a test entity.
|
||||
// @see \Drupal\language\DefaultLanguageItem
|
||||
$entity = EntityTest::create([
|
||||
'name' => 'Alexander',
|
||||
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
|
||||
]);
|
||||
$entity->save();
|
||||
// Ensure it is an instance of TranslatableInterface and that it is *not*
|
||||
// translatable.
|
||||
$this->assertInstanceOf(TranslatableInterface::class, $entity);
|
||||
$this->assertFalse($entity->isTranslatable());
|
||||
// Set up a test user with permission to view and update the test entity.
|
||||
$user = $this->drupalCreateUser([
|
||||
'view test entity',
|
||||
'administer entity_test content',
|
||||
]);
|
||||
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
|
||||
$request_options[RequestOptions::AUTH] = [
|
||||
$user->getAccountName(),
|
||||
$user->pass_raw,
|
||||
];
|
||||
// GET the test entity via JSON:API.
|
||||
$entity_url = Url::fromUri('internal:/jsonapi/entity_test/entity_test/' . $entity->uuid());
|
||||
$response = $this->request('GET', $entity_url, $request_options);
|
||||
$response_document = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
// Ensure that the entity's langcode attribute is 'und'.
|
||||
$this->assertSame(LanguageInterface::LANGCODE_NOT_SPECIFIED, $response_document['data']['attributes']['langcode']);
|
||||
// Prepare to PATCH the entity via JSON:API.
|
||||
$request_options[RequestOptions::HEADERS]['Content-Type'] = 'application/vnd.api+json';
|
||||
$request_options[RequestOptions::JSON] = [
|
||||
'data' => [
|
||||
'type' => 'entity_test--entity_test',
|
||||
'id' => $entity->uuid(),
|
||||
'attributes' => [
|
||||
'name' => 'Constantine',
|
||||
],
|
||||
],
|
||||
];
|
||||
// Issue the PATCH request and verify that the test entity was successfully
|
||||
// updated.
|
||||
$response = $this->request('PATCH', $entity_url, $request_options);
|
||||
$response_document = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame(200, $response->getStatusCode(), (string) $response->getBody());
|
||||
// Ensure that the entity's langcode attribute is still 'und' and the name
|
||||
// was successfully updated.
|
||||
$this->assertSame(LanguageInterface::LANGCODE_NOT_SPECIFIED, $response_document['data']['attributes']['langcode']);
|
||||
$this->assertSame('Constantine', $response_document['data']['attributes']['name']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure PATCHing a non-existing field property results in a helpful error.
|
||||
*
|
||||
* @see https://www.drupal.org/project/drupal/issues/3127883
|
||||
*/
|
||||
public function testPatchInvalidFieldPropertyFromIssue3127883(): void {
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
|
||||
// Set up data model.
|
||||
$this->drupalCreateContentType(['type' => 'page']);
|
||||
$this->rebuildAll();
|
||||
|
||||
// Create data.
|
||||
$node = Node::create([
|
||||
'title' => 'foo',
|
||||
'type' => 'page',
|
||||
'body' => [
|
||||
'format' => 'plain_text',
|
||||
'value' => 'Hello World',
|
||||
],
|
||||
]);
|
||||
$node->save();
|
||||
|
||||
// Test.
|
||||
$user = $this->drupalCreateUser(['bypass node access']);
|
||||
$url = Url::fromUri('internal:/jsonapi/node/page/' . $node->uuid());
|
||||
$request_options = [
|
||||
RequestOptions::HEADERS => [
|
||||
'Content-Type' => 'application/vnd.api+json',
|
||||
'Accept' => 'application/vnd.api+json',
|
||||
],
|
||||
RequestOptions::AUTH => [$user->getAccountName(), $user->pass_raw],
|
||||
RequestOptions::JSON => [
|
||||
'data' => [
|
||||
'type' => 'node--page',
|
||||
'id' => $node->uuid(),
|
||||
'attributes' => [
|
||||
'title' => 'Updated title',
|
||||
'body' => [
|
||||
'value' => 'Hello World … still.',
|
||||
// Intentional typo in the property name!
|
||||
'form' => 'plain_text',
|
||||
// Another intentional typo.
|
||||
// cSpell:disable-next-line
|
||||
'sumary' => 'Boring old "Hello World".',
|
||||
// And finally, one that is completely absurd.
|
||||
'foobar' => '<script>alert("HI!");</script>',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
$response = $this->request('PATCH', $url, $request_options);
|
||||
|
||||
// Assert a helpful error response is present.
|
||||
$data = $this->getDocumentFromResponse($response, FALSE);
|
||||
$this->assertSame(422, $response->getStatusCode());
|
||||
$this->assertNotNull($data);
|
||||
// cSpell:disable-next-line
|
||||
$this->assertSame("The properties 'form', 'sumary', 'foobar' do not exist on the 'body' field of type 'text_with_summary'. Writable properties are: 'value', 'format', 'summary'.", $data['errors'][0]['detail']);
|
||||
|
||||
$request_options = [
|
||||
RequestOptions::HEADERS => [
|
||||
'Content-Type' => 'application/vnd.api+json',
|
||||
'Accept' => 'application/vnd.api+json',
|
||||
],
|
||||
RequestOptions::AUTH => [$user->getAccountName(), $user->pass_raw],
|
||||
RequestOptions::JSON => [
|
||||
'data' => [
|
||||
'type' => 'node--page',
|
||||
'id' => $node->uuid(),
|
||||
'attributes' => [
|
||||
'title' => 'Updated title',
|
||||
'body' => [
|
||||
'value' => 'Hello World … still.',
|
||||
// Intentional typo in the property name!
|
||||
'form' => 'plain_text',
|
||||
// Another intentional typo.
|
||||
// cSpell:disable-next-line
|
||||
'sumary' => 'Boring old "Hello World".',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
$response = $this->request('PATCH', $url, $request_options);
|
||||
|
||||
// Assert a helpful error response is present.
|
||||
$data = $this->getDocumentFromResponse($response, FALSE);
|
||||
$this->assertSame(422, $response->getStatusCode());
|
||||
$this->assertNotNull($data);
|
||||
// cSpell:disable-next-line
|
||||
$this->assertSame("The properties 'form', 'sumary' do not exist on the 'body' field of type 'text_with_summary'. Did you mean 'format', 'summary'?", $data['errors'][0]['detail']);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,898 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\comment\Entity\Comment;
|
||||
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
|
||||
use Drupal\comment\Tests\CommentTestTrait;
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
|
||||
use Drupal\entity_test\Entity\EntityTestMapField;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\taxonomy\Entity\Term;
|
||||
use Drupal\taxonomy\Entity\Vocabulary;
|
||||
use Drupal\user\Entity\Role;
|
||||
use Drupal\user\Entity\User;
|
||||
use Drupal\user\RoleInterface;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
|
||||
/**
|
||||
* JSON:API regression tests.
|
||||
*
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class JsonApiRegressionTest extends JsonApiFunctionalTestBase {
|
||||
|
||||
use CommentTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'basic_auth',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* Ensure deep nested include works on multi target entity type field.
|
||||
*
|
||||
* @see https://www.drupal.org/project/drupal/issues/2973681
|
||||
*/
|
||||
public function testDeepNestedIncludeMultiTargetEntityTypeFieldFromIssue2973681(): void {
|
||||
// Set up data model.
|
||||
$this->assertTrue($this->container->get('module_installer')->install(['comment'], TRUE), 'Installed modules.');
|
||||
$this->addDefaultCommentField('node', 'article');
|
||||
$this->addDefaultCommentField('taxonomy_term', 'tags', 'comment', CommentItemInterface::OPEN, 'test_comment_type');
|
||||
$this->drupalCreateContentType(['type' => 'page']);
|
||||
$this->createEntityReferenceField(
|
||||
'node',
|
||||
'page',
|
||||
'field_comment',
|
||||
NULL,
|
||||
'comment',
|
||||
'default',
|
||||
[
|
||||
'target_bundles' => [
|
||||
'comment' => 'comment',
|
||||
'test_comment_type' => 'test_comment_type',
|
||||
],
|
||||
],
|
||||
FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
|
||||
);
|
||||
$this->rebuildAll();
|
||||
|
||||
// Create data.
|
||||
$node = Node::create([
|
||||
'title' => 'test article',
|
||||
'type' => 'article',
|
||||
]);
|
||||
$node->save();
|
||||
$comment = Comment::create([
|
||||
'subject' => 'Llama',
|
||||
'entity_id' => 1,
|
||||
'entity_type' => 'node',
|
||||
'field_name' => 'comment',
|
||||
]);
|
||||
$comment->save();
|
||||
$page = Node::create([
|
||||
'title' => 'test node',
|
||||
'type' => 'page',
|
||||
'field_comment' => [
|
||||
'entity' => $comment,
|
||||
],
|
||||
]);
|
||||
$page->save();
|
||||
|
||||
// Test.
|
||||
$user = $this->drupalCreateUser([
|
||||
'access content',
|
||||
'access comments',
|
||||
]);
|
||||
$response = $this->request('GET', Url::fromUri('internal:/jsonapi/node/page?include=field_comment,field_comment.entity_id,field_comment.entity_id.uid'), [
|
||||
RequestOptions::AUTH => [
|
||||
$user->getAccountName(),
|
||||
$user->pass_raw,
|
||||
],
|
||||
]);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures GETting terms works when multiple vocabularies exist.
|
||||
*
|
||||
* @see https://www.drupal.org/project/drupal/issues/2977879
|
||||
*/
|
||||
public function testGetTermWhenMultipleVocabulariesExistFromIssue2977879(): void {
|
||||
// Set up data model.
|
||||
$this->assertTrue($this->container->get('module_installer')->install(['taxonomy'], TRUE), 'Installed modules.');
|
||||
Vocabulary::create([
|
||||
'name' => 'one',
|
||||
'vid' => 'one',
|
||||
])->save();
|
||||
Vocabulary::create([
|
||||
'name' => 'two',
|
||||
'vid' => 'two',
|
||||
])->save();
|
||||
$this->rebuildAll();
|
||||
|
||||
// Create data.
|
||||
Term::create(['vid' => 'one'])
|
||||
->setName('Test')
|
||||
->save();
|
||||
|
||||
// Test.
|
||||
$user = $this->drupalCreateUser([
|
||||
'access content',
|
||||
]);
|
||||
$response = $this->request('GET', Url::fromUri('internal:/jsonapi/taxonomy_term/one'), [
|
||||
RequestOptions::AUTH => [
|
||||
$user->getAccountName(),
|
||||
$user->pass_raw,
|
||||
],
|
||||
]);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures GETting node collection + hook_node_grants() implementations works.
|
||||
*
|
||||
* @see https://www.drupal.org/project/drupal/issues/2984964
|
||||
*/
|
||||
public function testGetNodeCollectionWithHookNodeGrantsImplementationsFromIssue2984964(): void {
|
||||
// Set up data model.
|
||||
$this->assertTrue($this->container->get('module_installer')->install(['node_access_test'], TRUE), 'Installed modules.');
|
||||
node_access_rebuild();
|
||||
$this->rebuildAll();
|
||||
|
||||
// Create data.
|
||||
Node::create([
|
||||
'title' => 'test article',
|
||||
'type' => 'article',
|
||||
])->save();
|
||||
|
||||
// Test.
|
||||
$user = $this->drupalCreateUser([
|
||||
'access content',
|
||||
]);
|
||||
$response = $this->request('GET', Url::fromUri('internal:/jsonapi/node/article'), [
|
||||
RequestOptions::AUTH => [
|
||||
$user->getAccountName(),
|
||||
$user->pass_raw,
|
||||
],
|
||||
]);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
$this->assertContains('user.node_grants:view', explode(' ', $response->getHeader('X-Drupal-Cache-Contexts')[0]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cannot GET an entity with dangling references in an ER field.
|
||||
*
|
||||
* @see https://www.drupal.org/project/drupal/issues/2984647
|
||||
*/
|
||||
public function testDanglingReferencesInAnEntityReferenceFieldFromIssue2984647(): void {
|
||||
// Set up data model.
|
||||
$this->drupalCreateContentType(['type' => 'journal_issue']);
|
||||
$this->drupalCreateContentType(['type' => 'journal_conference']);
|
||||
$this->drupalCreateContentType(['type' => 'journal_article']);
|
||||
$this->createEntityReferenceField(
|
||||
'node',
|
||||
'journal_article',
|
||||
'field_issue',
|
||||
NULL,
|
||||
'node',
|
||||
'default',
|
||||
[
|
||||
'target_bundles' => [
|
||||
'journal_issue' => 'journal_issue',
|
||||
],
|
||||
],
|
||||
FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
|
||||
);
|
||||
$this->createEntityReferenceField(
|
||||
'node',
|
||||
'journal_article',
|
||||
'field_mentioned_in',
|
||||
NULL,
|
||||
'node',
|
||||
'default',
|
||||
[
|
||||
'target_bundles' => [
|
||||
'journal_issue' => 'journal_issue',
|
||||
'journal_conference' => 'journal_conference',
|
||||
],
|
||||
],
|
||||
FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
|
||||
);
|
||||
$this->rebuildAll();
|
||||
|
||||
// Create data.
|
||||
$issue_node = Node::create([
|
||||
'title' => 'Test Journal Issue',
|
||||
'type' => 'journal_issue',
|
||||
]);
|
||||
$issue_node->save();
|
||||
$conference_node = Node::create([
|
||||
'title' => 'First Journal Conference!',
|
||||
'type' => 'journal_conference',
|
||||
]);
|
||||
$conference_node->save();
|
||||
|
||||
$user = $this->drupalCreateUser([
|
||||
'access content',
|
||||
'edit own journal_article content',
|
||||
]);
|
||||
$article_node = Node::create([
|
||||
'title' => 'Test Journal Article',
|
||||
'type' => 'journal_article',
|
||||
'field_issue' => [
|
||||
['target_id' => $issue_node->id()],
|
||||
],
|
||||
'field_mentioned_in' => [
|
||||
['target_id' => $issue_node->id()],
|
||||
['target_id' => $conference_node->id()],
|
||||
],
|
||||
]);
|
||||
$article_node->setOwner($user);
|
||||
$article_node->save();
|
||||
|
||||
// Test.
|
||||
$url = Url::fromUri(sprintf('internal:/jsonapi/node/journal_article/%s', $article_node->uuid()));
|
||||
$request_options = [
|
||||
RequestOptions::HEADERS => [
|
||||
'Content-Type' => 'application/vnd.api+json',
|
||||
'Accept' => 'application/vnd.api+json',
|
||||
],
|
||||
RequestOptions::AUTH => [$user->getAccountName(), $user->pass_raw],
|
||||
];
|
||||
$issue_node->delete();
|
||||
$response = $this->request('GET', $url, $request_options);
|
||||
$document = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
|
||||
// Entity reference field allowing a single bundle: dangling reference's
|
||||
// resource type is deduced.
|
||||
$this->assertSame([
|
||||
[
|
||||
'type' => 'node--journal_issue',
|
||||
'id' => 'missing',
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'help' => [
|
||||
'href' => 'https://www.drupal.org/docs/8/modules/json-api/core-concepts#missing',
|
||||
'meta' => [
|
||||
'about' => "Usage and meaning of the 'missing' resource identifier.",
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
], $document['data']['relationships']['field_issue']['data']);
|
||||
|
||||
// Entity reference field allowing multiple bundles: dangling reference's
|
||||
// resource type is NOT deduced.
|
||||
$this->assertSame([
|
||||
[
|
||||
'type' => 'unknown',
|
||||
'id' => 'missing',
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'help' => [
|
||||
'href' => 'https://www.drupal.org/docs/8/modules/json-api/core-concepts#missing',
|
||||
'meta' => [
|
||||
'about' => "Usage and meaning of the 'missing' resource identifier.",
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'node--journal_conference',
|
||||
'id' => $conference_node->uuid(),
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => (int) $conference_node->id(),
|
||||
],
|
||||
],
|
||||
], $document['data']['relationships']['field_mentioned_in']['data']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that JSON:API routes are caches are dynamically rebuilt.
|
||||
*
|
||||
* Adding a new relationship field should cause new routes to be immediately
|
||||
* regenerated. The site builder should not need to manually rebuild caches.
|
||||
*
|
||||
* @see https://www.drupal.org/project/drupal/issues/2984886
|
||||
*/
|
||||
public function testThatRoutesAreRebuiltAfterDataModelChangesFromIssue2984886(): void {
|
||||
$user = $this->drupalCreateUser(['access content']);
|
||||
$request_options = [
|
||||
RequestOptions::AUTH => [
|
||||
$user->getAccountName(),
|
||||
$user->pass_raw,
|
||||
],
|
||||
];
|
||||
|
||||
$response = $this->request('GET', Url::fromUri('internal:/jsonapi/node/dog'), $request_options);
|
||||
$this->assertSame(404, $response->getStatusCode());
|
||||
|
||||
$node_type_dog = NodeType::create([
|
||||
'type' => 'dog',
|
||||
'name' => 'Dog',
|
||||
]);
|
||||
$node_type_dog->save();
|
||||
NodeType::create([
|
||||
'type' => 'cat',
|
||||
'name' => 'Cat',
|
||||
])->save();
|
||||
\Drupal::service('router.builder')->rebuildIfNeeded();
|
||||
|
||||
$response = $this->request('GET', Url::fromUri('internal:/jsonapi/node/dog'), $request_options);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
|
||||
$this->createEntityReferenceField('node', 'dog', 'field_test', '', 'node');
|
||||
\Drupal::service('router.builder')->rebuildIfNeeded();
|
||||
|
||||
$dog = Node::create(['type' => 'dog', 'title' => 'retriever']);
|
||||
$dog->save();
|
||||
|
||||
$response = $this->request('GET', Url::fromUri('internal:/jsonapi/node/dog/' . $dog->uuid() . '/field_test'), $request_options);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
|
||||
$this->createEntityReferenceField('node', 'cat', 'field_test', '', 'node');
|
||||
\Drupal::service('router.builder')->rebuildIfNeeded();
|
||||
|
||||
$cat = Node::create(['type' => 'cat', 'title' => 'E. Napoleon']);
|
||||
$cat->save();
|
||||
|
||||
$response = $this->request('GET', Url::fromUri('internal:/jsonapi/node/cat/' . $cat->uuid() . '/field_test'), $request_options);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
|
||||
FieldConfig::loadByName('node', 'cat', 'field_test')->delete();
|
||||
\Drupal::service('router.builder')->rebuildIfNeeded();
|
||||
|
||||
$response = $this->request('GET', Url::fromUri('internal:/jsonapi/node/cat/' . $cat->uuid() . '/field_test'), $request_options);
|
||||
$this->assertSame(404, $response->getStatusCode());
|
||||
|
||||
$node_type_dog->delete();
|
||||
\Drupal::service('router.builder')->rebuildIfNeeded();
|
||||
|
||||
$response = $this->request('GET', Url::fromUri('internal:/jsonapi/node/dog'), $request_options);
|
||||
$this->assertSame(404, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures denormalizing relationships with aliased field names works.
|
||||
*
|
||||
* @see https://www.drupal.org/project/drupal/issues/3007113
|
||||
* @see https://www.drupal.org/project/jsonapi_extras/issues/3004582#comment-12817261
|
||||
*/
|
||||
public function testDenormalizeAliasedRelationshipFromIssue2953207(): void {
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
|
||||
// Since the JSON:API module does not have an explicit mechanism to set up
|
||||
// field aliases, create a strange data model so that automatic aliasing
|
||||
// allows us to test aliased relationships.
|
||||
// @see \Drupal\jsonapi\ResourceType\ResourceTypeRepository::getFieldMapping()
|
||||
$internal_relationship_field_name = 'type';
|
||||
$public_relationship_field_name = 'taxonomy_term_' . $internal_relationship_field_name;
|
||||
|
||||
// Set up data model.
|
||||
$this->createEntityReferenceField(
|
||||
'taxonomy_term',
|
||||
'tags',
|
||||
$internal_relationship_field_name,
|
||||
NULL,
|
||||
'user'
|
||||
);
|
||||
$this->rebuildAll();
|
||||
|
||||
// Create data.
|
||||
Term::create([
|
||||
'name' => 'foobar',
|
||||
'vid' => 'tags',
|
||||
'type' => ['target_id' => 1],
|
||||
])->save();
|
||||
|
||||
// Test.
|
||||
$user = $this->drupalCreateUser([
|
||||
'edit terms in tags',
|
||||
]);
|
||||
$body = [
|
||||
'data' => [
|
||||
'type' => 'user--user',
|
||||
'id' => User::load(0)->uuid(),
|
||||
],
|
||||
];
|
||||
|
||||
// Test.
|
||||
$response = $this->request('PATCH', Url::fromUri(sprintf('internal:/jsonapi/taxonomy_term/tags/%s/relationships/%s', Term::load(1)->uuid(), $public_relationship_field_name)), [
|
||||
RequestOptions::AUTH => [
|
||||
$user->getAccountName(),
|
||||
$user->pass_raw,
|
||||
],
|
||||
RequestOptions::HEADERS => [
|
||||
'Content-Type' => 'application/vnd.api+json',
|
||||
],
|
||||
RequestOptions::BODY => Json::encode($body),
|
||||
]);
|
||||
$this->assertSame(204, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that Drupal's page cache is effective.
|
||||
*
|
||||
* @see https://www.drupal.org/project/drupal/issues/3009596
|
||||
*/
|
||||
public function testPageCacheFromIssue3009596(): void {
|
||||
$anonymous_role = Role::load(RoleInterface::ANONYMOUS_ID);
|
||||
$anonymous_role->grantPermission('access content');
|
||||
$anonymous_role->trustData()->save();
|
||||
|
||||
NodeType::create([
|
||||
'type' => 'emu_fact',
|
||||
'name' => 'Emu Fact',
|
||||
])->save();
|
||||
\Drupal::service('router.builder')->rebuildIfNeeded();
|
||||
|
||||
$node = Node::create([
|
||||
'type' => 'emu_fact',
|
||||
'title' => "Emus don't say moo!",
|
||||
]);
|
||||
$node->save();
|
||||
|
||||
$request_options = [
|
||||
RequestOptions::HEADERS => ['Accept' => 'application/vnd.api+json'],
|
||||
];
|
||||
$node_url = Url::fromUri('internal:/jsonapi/node/emu_fact/' . $node->uuid());
|
||||
|
||||
// The first request should be a cache MISS.
|
||||
$response = $this->request('GET', $node_url, $request_options);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
$this->assertSame('MISS', $response->getHeader('X-Drupal-Cache')[0]);
|
||||
|
||||
// The second request should be a cache HIT.
|
||||
$response = $this->request('GET', $node_url, $request_options);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
$this->assertSame('HIT', $response->getHeader('X-Drupal-Cache')[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures datetime fields are normalized using the correct timezone.
|
||||
*
|
||||
* @see https://www.drupal.org/project/drupal/issues/2999438
|
||||
*/
|
||||
public function testPatchingDateTimeNormalizedWrongTimeZoneIssue3021194(): void {
|
||||
// Set up data model.
|
||||
$this->assertTrue($this->container->get('module_installer')->install(['datetime'], TRUE), 'Installed modules.');
|
||||
$this->drupalCreateContentType(['type' => 'page']);
|
||||
$this->rebuildAll();
|
||||
FieldStorageConfig::create([
|
||||
'field_name' => 'when',
|
||||
'type' => 'datetime',
|
||||
'entity_type' => 'node',
|
||||
'settings' => ['datetime_type' => DateTimeItem::DATETIME_TYPE_DATETIME],
|
||||
])
|
||||
->save();
|
||||
FieldConfig::create([
|
||||
'field_name' => 'when',
|
||||
'entity_type' => 'node',
|
||||
'bundle' => 'page',
|
||||
])
|
||||
->save();
|
||||
|
||||
// Create data.
|
||||
$page = Node::create([
|
||||
'title' => 'Stegosaurus',
|
||||
'type' => 'page',
|
||||
'when' => [
|
||||
'value' => '2018-09-16T12:00:00',
|
||||
],
|
||||
]);
|
||||
$page->save();
|
||||
|
||||
// Test.
|
||||
$user = $this->drupalCreateUser([
|
||||
'access content',
|
||||
]);
|
||||
$response = $this->request('GET', Url::fromUri('internal:/jsonapi/node/page/' . $page->uuid()), [
|
||||
RequestOptions::AUTH => [
|
||||
$user->getAccountName(),
|
||||
$user->pass_raw,
|
||||
],
|
||||
]);
|
||||
$doc = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
$this->assertSame('2018-09-16T22:00:00+10:00', $doc['data']['attributes']['when']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure includes are respected even when POSTing.
|
||||
*
|
||||
* @see https://www.drupal.org/project/drupal/issues/3026030
|
||||
*/
|
||||
public function testPostToIncludeUrlDoesNotReturnIncludeFromIssue3026030(): void {
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
|
||||
// Set up data model.
|
||||
$this->drupalCreateContentType(['type' => 'page']);
|
||||
$this->rebuildAll();
|
||||
|
||||
// Test.
|
||||
$user = $this->drupalCreateUser(['bypass node access']);
|
||||
$url = Url::fromUri('internal:/jsonapi/node/page?include=uid');
|
||||
$request_options = [
|
||||
RequestOptions::HEADERS => [
|
||||
'Content-Type' => 'application/vnd.api+json',
|
||||
'Accept' => 'application/vnd.api+json',
|
||||
],
|
||||
RequestOptions::AUTH => [$user->getAccountName(), $user->pass_raw],
|
||||
RequestOptions::JSON => [
|
||||
'data' => [
|
||||
'type' => 'node--page',
|
||||
'attributes' => [
|
||||
'title' => 'test',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
$response = $this->request('POST', $url, $request_options);
|
||||
$doc = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame(201, $response->getStatusCode());
|
||||
$this->assertArrayHasKey('included', $doc);
|
||||
$this->assertSame($user->label(), $doc['included'][0]['attributes']['name']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure `@FieldType=map` fields are normalized correctly.
|
||||
*
|
||||
* @see https://www.drupal.org/project/drupal/issues/3040590
|
||||
*/
|
||||
public function testMapFieldTypeNormalizationFromIssue3040590(): void {
|
||||
$this->assertTrue($this->container->get('module_installer')->install(['entity_test'], TRUE), 'Installed modules.');
|
||||
|
||||
// Create data.
|
||||
$entity_a = EntityTestMapField::create([
|
||||
'name' => 'A',
|
||||
'data' => [
|
||||
'foo' => 'bar',
|
||||
'baz' => 'qux',
|
||||
],
|
||||
]);
|
||||
$entity_a->save();
|
||||
$entity_b = EntityTestMapField::create([
|
||||
'name' => 'B',
|
||||
]);
|
||||
$entity_b->save();
|
||||
$user = $this->drupalCreateUser([
|
||||
'administer entity_test content',
|
||||
]);
|
||||
|
||||
// Test.
|
||||
$url = Url::fromUri('internal:/jsonapi/entity_test_map_field/entity_test_map_field?sort=drupal_internal__id');
|
||||
$request_options = [
|
||||
RequestOptions::AUTH => [$user->getAccountName(), $user->pass_raw],
|
||||
];
|
||||
$response = $this->request('GET', $url, $request_options);
|
||||
$data = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
$this->assertSame([
|
||||
'foo' => 'bar',
|
||||
'baz' => 'qux',
|
||||
], $data['data'][0]['attributes']['data']);
|
||||
$this->assertNull($data['data'][1]['attributes']['data']);
|
||||
$entity_a->set('data', [
|
||||
'foo' => 'bar',
|
||||
])->save();
|
||||
$response = $this->request('GET', $url, $request_options);
|
||||
$data = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
$this->assertSame(['foo' => 'bar'], $data['data'][0]['attributes']['data']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the response still has meaningful error messages.
|
||||
*/
|
||||
public function testRecursionDetectedWhenResponseContainsViolationsFrom3042124(): void {
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
|
||||
// Set up default request.
|
||||
$url = Url::fromUri('internal:/jsonapi/node/article');
|
||||
$request_options = [
|
||||
RequestOptions::HEADERS => [
|
||||
'Content-Type' => 'application/vnd.api+json',
|
||||
'Accept' => 'application/vnd.api+json',
|
||||
],
|
||||
RequestOptions::JSON => [
|
||||
'data' => [
|
||||
'type' => 'node--article',
|
||||
'attributes' => [],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// Set up test users.
|
||||
$user = $this->drupalCreateUser(['bypass node access'], 'Sam');
|
||||
$admin = $this->drupalCreateUser([], 'Gandalf', TRUE);
|
||||
|
||||
// Make request as regular user.
|
||||
$request_options[RequestOptions::AUTH] = [$user->getAccountName(), $user->pass_raw];
|
||||
$this->request('POST', $url, $request_options);
|
||||
$response = $this->request('POST', $url, $request_options);
|
||||
|
||||
// Assert that the response has a body.
|
||||
$data = $this->getDocumentFromResponse($response, FALSE);
|
||||
$this->assertSame(422, $response->getStatusCode());
|
||||
$this->assertNotNull($data);
|
||||
$this->assertSame(sprintf('title: This value should not be null.'), $data['errors'][0]['detail']);
|
||||
|
||||
// Make request as regular user.
|
||||
$request_options[RequestOptions::AUTH] = [$admin->getAccountName(), $admin->pass_raw];
|
||||
$this->request('POST', $url, $request_options);
|
||||
$response = $this->request('POST', $url, $request_options);
|
||||
|
||||
// Assert that the response has a body.
|
||||
$data = $this->getDocumentFromResponse($response, FALSE);
|
||||
$this->assertSame(422, $response->getStatusCode());
|
||||
$this->assertNotNull($data);
|
||||
$this->assertSame(sprintf('title: This value should not be null.'), $data['errors'][0]['detail']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure POSTing invalid data results in a 422 response, not a PHP error.
|
||||
*
|
||||
* @see https://www.drupal.org/project/drupal/issues/3052954
|
||||
*/
|
||||
public function testInvalidDataTriggersUnprocessableEntityErrorFromIssue3052954(): void {
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
|
||||
// Set up data model.
|
||||
$user = $this->drupalCreateUser(['bypass node access']);
|
||||
|
||||
// Test.
|
||||
$request_options = [
|
||||
RequestOptions::HEADERS => [
|
||||
'Content-Type' => 'application/vnd.api+json',
|
||||
'Accept' => 'application/vnd.api+json',
|
||||
],
|
||||
RequestOptions::JSON => [
|
||||
'data' => [
|
||||
'type' => 'article',
|
||||
'attributes' => [
|
||||
'title' => 'foobar',
|
||||
'created' => 'not_a_date',
|
||||
],
|
||||
],
|
||||
],
|
||||
RequestOptions::AUTH => [$user->getAccountName(), $user->pass_raw],
|
||||
];
|
||||
$response = $this->request('POST', Url::fromUri('internal:/jsonapi/node/article'), $request_options);
|
||||
$this->assertSame(422, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure optional `@FieldType=map` fields are denormalized correctly.
|
||||
*/
|
||||
public function testEmptyMapFieldTypeDenormalization(): void {
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
|
||||
// Set up data model.
|
||||
$this->assertTrue($this->container->get('module_installer')->install(['entity_test'], TRUE), 'Installed modules.');
|
||||
|
||||
// Create data.
|
||||
$entity = EntityTestMapField::create([
|
||||
'name' => 'foo',
|
||||
]);
|
||||
$entity->save();
|
||||
$user = $this->drupalCreateUser([
|
||||
'administer entity_test content',
|
||||
]);
|
||||
|
||||
// Test.
|
||||
$url = Url::fromUri(sprintf('internal:/jsonapi/entity_test_map_field/entity_test_map_field/%s', $entity->uuid()));
|
||||
$request_options = [
|
||||
RequestOptions::AUTH => [$user->getAccountName(), $user->pass_raw],
|
||||
];
|
||||
// Retrieve the current representation of the entity.
|
||||
$response = $this->request('GET', $url, $request_options);
|
||||
$doc = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
// Modify the title. The @FieldType=map normalization is not changed. (The
|
||||
// name of this field is confusingly also 'data'.)
|
||||
$doc['data']['attributes']['name'] = 'bar';
|
||||
$request_options[RequestOptions::HEADERS] = [
|
||||
'Content-Type' => 'application/vnd.api+json',
|
||||
'Accept' => 'application/vnd.api+json',
|
||||
];
|
||||
$request_options[RequestOptions::BODY] = Json::encode($doc);
|
||||
$response = $this->request('PATCH', $url, $request_options);
|
||||
$patched_document = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
$this->assertSame($doc['data']['attributes']['data'], $patched_document['data']['attributes']['data']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure EntityAccessDeniedHttpException cacheability is taken into account.
|
||||
*/
|
||||
public function testLeakCacheMetadataInOmitted(): void {
|
||||
$term = Term::create([
|
||||
'name' => 'Llama term',
|
||||
'vid' => 'tags',
|
||||
]);
|
||||
$term->setUnpublished();
|
||||
$term->save();
|
||||
|
||||
$node = Node::create([
|
||||
'type' => 'article',
|
||||
'title' => 'Llama node',
|
||||
'field_tags' => ['target_id' => $term->id()],
|
||||
]);
|
||||
$node->save();
|
||||
|
||||
$user = $this->drupalCreateUser([
|
||||
'access content',
|
||||
]);
|
||||
$request_options = [
|
||||
RequestOptions::AUTH => [
|
||||
$user->getAccountName(),
|
||||
$user->pass_raw,
|
||||
],
|
||||
];
|
||||
|
||||
// Request with unpublished term. At this point it would include the term
|
||||
// into "omitted" part of the response. The point here is that we
|
||||
// purposefully warm up the cache where it is excluded from response and
|
||||
// on the next run we will assure merely publishing term is enough to make
|
||||
// it visible, i.e. that the 1st response was invalidated in Drupal cache.
|
||||
$url = Url::fromUri('internal:/jsonapi/' . $node->getEntityTypeId() . '/' . $node->bundle(), [
|
||||
'query' => ['include' => 'field_tags'],
|
||||
]);
|
||||
$response = $this->request('GET', $url, $request_options);
|
||||
$document = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
|
||||
$this->assertArrayNotHasKey('included', $document, 'JSON API response does not contain "included" taxonomy term as the latter is not published, i.e not accessible.');
|
||||
|
||||
$omitted = $document['meta']['omitted']['links'];
|
||||
unset($omitted['help']);
|
||||
$omitted = reset($omitted);
|
||||
$expected_url = Url::fromUri('internal:/jsonapi/' . $term->getEntityTypeId() . '/' . $term->bundle() . '/' . $term->uuid());
|
||||
$expected_url->setAbsolute();
|
||||
$this->assertSame($expected_url->toString(), $omitted['href'], 'Entity that is excluded due to access constraints is correctly reported in the "Omitted" section of the JSON API response.');
|
||||
|
||||
$term->setPublished();
|
||||
$term->save();
|
||||
$response = $this->request('GET', $url, $request_options);
|
||||
$document = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
$this->assertEquals($term->uuid(), $document['included'][0]['id'], 'JSON API response contains "included" taxonomy term as it became published, i.e accessible.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that "virtual/missing" resources can exist for renamed fields.
|
||||
*
|
||||
* @see https://www.drupal.org/project/drupal/issues/3034786
|
||||
* @see https://www.drupal.org/project/drupal/issues/3035544
|
||||
*/
|
||||
public function testAliasedFieldsWithVirtualRelationships(): void {
|
||||
// Set up the data model.
|
||||
$this->assertTrue($this->container->get('module_installer')->install([
|
||||
'taxonomy',
|
||||
'jsonapi_test_resource_type_building',
|
||||
], TRUE), 'Installed modules.');
|
||||
\Drupal::state()->set('jsonapi_test_resource_type_builder.resource_type_field_aliases', [
|
||||
'node--article' => [
|
||||
'field_tags' => 'field_aliased',
|
||||
],
|
||||
]);
|
||||
$this->rebuildAll();
|
||||
|
||||
$tag_term = Term::create([
|
||||
'vid' => 'tags',
|
||||
'name' => 'test_tag',
|
||||
]);
|
||||
$tag_term->save();
|
||||
|
||||
$article_node = Node::create([
|
||||
'type' => 'article',
|
||||
'title' => 'test_article',
|
||||
'field_tags' => ['target_id' => $tag_term->id()],
|
||||
]);
|
||||
$article_node->save();
|
||||
|
||||
// Make a broken reference.
|
||||
$tag_term->delete();
|
||||
|
||||
// Make sure that accessing a node that references a deleted term does not
|
||||
// cause an error.
|
||||
$user = $this->drupalCreateUser(['bypass node access']);
|
||||
$request_options = [
|
||||
RequestOptions::AUTH => [
|
||||
$user->getAccountName(),
|
||||
$user->pass_raw,
|
||||
],
|
||||
];
|
||||
$response = $this->request('GET', Url::fromUri('internal:/jsonapi/node/article/' . $article_node->uuid()), $request_options);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that caching isn't happening for non-cacheable methods.
|
||||
*
|
||||
* @see https://www.drupal.org/project/drupal/issues/3072076
|
||||
*/
|
||||
public function testNonCacheableMethods(): void {
|
||||
$this->container->get('module_installer')->install([
|
||||
'jsonapi_test_non_cacheable_methods',
|
||||
], TRUE);
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
|
||||
$node = Node::create([
|
||||
'type' => 'article',
|
||||
'title' => 'Llama non-cacheable',
|
||||
]);
|
||||
$node->save();
|
||||
|
||||
$user = $this->drupalCreateUser([
|
||||
'access content',
|
||||
'create article content',
|
||||
'edit any article content',
|
||||
'delete any article content',
|
||||
]);
|
||||
$base_request_options = [
|
||||
RequestOptions::HEADERS => [
|
||||
'Content-Type' => 'application/vnd.api+json',
|
||||
'Accept' => 'application/vnd.api+json',
|
||||
],
|
||||
RequestOptions::AUTH => [$user->getAccountName(), $user->pass_raw],
|
||||
];
|
||||
$methods = [
|
||||
'HEAD',
|
||||
'GET',
|
||||
];
|
||||
foreach ($methods as $method) {
|
||||
$response = $this->request($method, Url::fromUri('internal:/jsonapi/node/article/' . $node->uuid()), $base_request_options);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
$patch_request_options = $base_request_options + [
|
||||
RequestOptions::JSON => [
|
||||
'data' => [
|
||||
'type' => 'node--article',
|
||||
'id' => $node->uuid(),
|
||||
],
|
||||
],
|
||||
];
|
||||
$response = $this->request('PATCH', Url::fromUri('internal:/jsonapi/node/article/' . $node->uuid()), $patch_request_options);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
|
||||
$response = $this->request('DELETE', Url::fromUri('internal:/jsonapi/node/article/' . $node->uuid()), $base_request_options);
|
||||
$this->assertSame(204, $response->getStatusCode());
|
||||
|
||||
$post_request_options = $base_request_options + [
|
||||
RequestOptions::JSON => [
|
||||
'data' => [
|
||||
'type' => 'node--article',
|
||||
'attributes' => [
|
||||
'title' => 'Llama non-cacheable',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
$response = $this->request('POST', Url::fromUri('internal:/jsonapi/node/article'), $post_request_options);
|
||||
$this->assertSame(201, $response->getStatusCode());
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\entity_test\EntityTestHelper;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
|
||||
/**
|
||||
* JSON:API resource tests.
|
||||
*
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class JsonApiRelationshipTest extends JsonApiFunctionalTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'basic_auth',
|
||||
'entity_test',
|
||||
'jsonapi_test_field_type',
|
||||
];
|
||||
|
||||
/**
|
||||
* The entity type ID.
|
||||
*/
|
||||
protected string $entityTypeId = 'entity_test';
|
||||
|
||||
/**
|
||||
* The entity bundle.
|
||||
*/
|
||||
protected string $bundle = 'entity_test';
|
||||
|
||||
/**
|
||||
* The field name.
|
||||
*/
|
||||
protected string $fieldName = 'field_child';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
EntityTestHelper::createBundle($this->bundle, 'Parent', $this->entityTypeId);
|
||||
|
||||
FieldStorageConfig::create([
|
||||
'field_name' => $this->fieldName,
|
||||
'type' => 'jsonapi_test_field_type_entity_reference_uuid',
|
||||
'entity_type' => $this->entityTypeId,
|
||||
'cardinality' => 1,
|
||||
'settings' => [
|
||||
'target_type' => $this->entityTypeId,
|
||||
],
|
||||
])->save();
|
||||
FieldConfig::create([
|
||||
'field_name' => $this->fieldName,
|
||||
'entity_type' => $this->entityTypeId,
|
||||
'bundle' => $this->bundle,
|
||||
'label' => $this->randomString(),
|
||||
'settings' => [
|
||||
'handler' => 'default',
|
||||
'handler_settings' => [],
|
||||
],
|
||||
])->save();
|
||||
|
||||
\Drupal::service('router.builder')->rebuild();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test relationships without target_id as main property.
|
||||
*
|
||||
* @see https://www.drupal.org/project/drupal/issues/3476224
|
||||
*/
|
||||
public function testPatchHandleUUIDPropertyReferenceFieldIssue3127883(): void {
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
$user = $this->drupalCreateUser([
|
||||
'administer entity_test content',
|
||||
'view test entity',
|
||||
]);
|
||||
|
||||
// Create parent and child entities.
|
||||
$storage = $this->container->get('entity_type.manager')
|
||||
->getStorage($this->entityTypeId);
|
||||
$parentEntity = $storage
|
||||
->create([
|
||||
'type' => $this->bundle,
|
||||
]);
|
||||
$parentEntity->save();
|
||||
$childUuid = $this->container->get('uuid')->generate();
|
||||
$childEntity = $storage
|
||||
->create([
|
||||
'type' => $this->bundle,
|
||||
'uuid' => $childUuid,
|
||||
]);
|
||||
$childEntity->save();
|
||||
$uuid = $childEntity->uuid();
|
||||
$this->assertEquals($childUuid, $uuid);
|
||||
|
||||
// 1. Successful PATCH to the related endpoint.
|
||||
$url = Url::fromUri(sprintf('internal:/jsonapi/%s/%s/%s/relationships/%s', $this->entityTypeId, $this->bundle, $parentEntity->uuid(), $this->fieldName));
|
||||
$request_options = [
|
||||
RequestOptions::HEADERS => [
|
||||
'Content-Type' => 'application/vnd.api+json',
|
||||
'Accept' => 'application/vnd.api+json',
|
||||
],
|
||||
RequestOptions::AUTH => [$user->getAccountName(), $user->pass_raw],
|
||||
RequestOptions::JSON => [
|
||||
'data' => [
|
||||
'id' => $childUuid,
|
||||
'type' => sprintf('%s--%s', $this->entityTypeId, $this->bundle),
|
||||
],
|
||||
],
|
||||
];
|
||||
$response = $this->request('PATCH', $url, $request_options);
|
||||
$this->assertSame(204, $response->getStatusCode(), (string) $response->getBody());
|
||||
$parentEntity = $storage->loadUnchanged($parentEntity->id());
|
||||
$this->assertEquals($childEntity->uuid(), $parentEntity->get($this->fieldName)->target_uuid);
|
||||
|
||||
// Reset the relationship.
|
||||
$parentEntity->set($this->fieldName, NULL)
|
||||
->save();
|
||||
$parentEntity = $storage->loadUnchanged($parentEntity->id());
|
||||
$this->assertTrue($parentEntity->get($this->fieldName)->isEmpty());
|
||||
|
||||
// 2. Successful PATCH to individual endpoint.
|
||||
$url = Url::fromUri(sprintf('internal:/jsonapi/%s/%s/%s', $this->entityTypeId, $this->bundle, $parentEntity->uuid()));
|
||||
$request_options[RequestOptions::JSON] = [
|
||||
'data' => [
|
||||
'id' => $parentEntity->uuid(),
|
||||
'type' => sprintf('%s--%s', $this->entityTypeId, $this->bundle),
|
||||
'relationships' => [
|
||||
$this->fieldName => [
|
||||
'data' => [
|
||||
[
|
||||
'id' => $childUuid,
|
||||
'type' => sprintf('%s--%s', $this->entityTypeId, $this->bundle),
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
$response = $this->request('PATCH', $url, $request_options);
|
||||
$this->assertSame(200, $response->getStatusCode(), (string) $response->getBody());
|
||||
$parentEntity = $storage->loadUnchanged($parentEntity->id());
|
||||
$this->assertEquals($childEntity->uuid(), $parentEntity->get($this->fieldName)->target_uuid);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\Tests\ApiRequestTrait;
|
||||
|
||||
/**
|
||||
* Boilerplate for JSON:API Functional tests' HTTP requests.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait JsonApiRequestTestTrait {
|
||||
use ApiRequestTrait {
|
||||
makeApiRequest as request;
|
||||
}
|
||||
|
||||
}
|
||||
413
web/core/modules/jsonapi/tests/src/Functional/MediaTest.php
Normal file
413
web/core/modules/jsonapi/tests/src/Functional/MediaTest.php
Normal file
@ -0,0 +1,413 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\file\Entity\File;
|
||||
use Drupal\media\Entity\Media;
|
||||
use Drupal\media\Entity\MediaType;
|
||||
use Drupal\Tests\jsonapi\Traits\CommonCollectionFilterAccessTestPatternsTrait;
|
||||
use Drupal\user\Entity\User;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "Media" content entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class MediaTest extends ResourceTestBase {
|
||||
|
||||
use CommonCollectionFilterAccessTestPatternsTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['media'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'media';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'media--camelids';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeIsVersionable = TRUE;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\media\MediaInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $patchProtectedFieldNames = [
|
||||
'changed' => NULL,
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
$this->grantPermissionsToTestedRole(['view media', 'view any camelids media revisions']);
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
$this->grantPermissionsToTestedRole(['create camelids media', 'access content']);
|
||||
break;
|
||||
|
||||
case 'PATCH':
|
||||
$this->grantPermissionsToTestedRole(['edit any camelids media']);
|
||||
// @todo Remove this in https://www.drupal.org/node/2824851.
|
||||
$this->grantPermissionsToTestedRole(['access content']);
|
||||
break;
|
||||
|
||||
case 'DELETE':
|
||||
$this->grantPermissionsToTestedRole(['delete any camelids media']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpRevisionAuthorization($method): void {
|
||||
parent::setUpRevisionAuthorization($method);
|
||||
$this->grantPermissionsToTestedRole(['view all media revisions']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
if (!MediaType::load('camelids')) {
|
||||
// Create a "Camelids" media type.
|
||||
$media_type = MediaType::create([
|
||||
'label' => 'Camelids',
|
||||
'id' => 'camelids',
|
||||
'description' => 'Camelids are large, strictly herbivorous animals with slender necks and long legs.',
|
||||
'source' => 'file',
|
||||
]);
|
||||
$media_type->save();
|
||||
// Create the source field.
|
||||
$source_field = $media_type->getSource()->createSourceField($media_type);
|
||||
$source_field->getFieldStorageDefinition()->save();
|
||||
$source_field->save();
|
||||
$media_type
|
||||
->set('source_configuration', [
|
||||
'source_field' => $source_field->getName(),
|
||||
])
|
||||
->save();
|
||||
}
|
||||
|
||||
// Create a file to upload.
|
||||
$file = File::create([
|
||||
'uri' => 'public://llama.txt',
|
||||
]);
|
||||
$file->setPermanent();
|
||||
$file->save();
|
||||
|
||||
// @see \Drupal\Tests\jsonapi\Functional\MediaTest::testPostIndividual()
|
||||
$post_file = File::create([
|
||||
'uri' => 'public://llama2.txt',
|
||||
]);
|
||||
$post_file->setPermanent();
|
||||
$post_file->save();
|
||||
|
||||
// Create a "Llama" media item.
|
||||
$media = Media::create([
|
||||
'bundle' => 'camelids',
|
||||
'field_media_file' => [
|
||||
'target_id' => $file->id(),
|
||||
],
|
||||
]);
|
||||
$media
|
||||
->setName('Llama')
|
||||
->setPublished()
|
||||
->setCreatedTime(123456789)
|
||||
->setOwnerId($this->account->id())
|
||||
->setRevisionUserId($this->account->id())
|
||||
->save();
|
||||
|
||||
return $media;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$file = File::load(1);
|
||||
$thumbnail = File::load(3);
|
||||
$author = User::load($this->entity->getOwnerId());
|
||||
$base_url = Url::fromUri('base:/jsonapi/media/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' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $base_url->toString()],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'media--camelids',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url->toString()],
|
||||
],
|
||||
'attributes' => [
|
||||
'langcode' => 'en',
|
||||
'name' => 'Llama',
|
||||
'status' => TRUE,
|
||||
'created' => '1973-11-29T21:33:09+00:00',
|
||||
'changed' => (new \DateTime())->setTimestamp($this->entity->getChangedTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
|
||||
'revision_created' => (new \DateTime())->setTimestamp((int) $this->entity->getRevisionCreationTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
|
||||
'default_langcode' => TRUE,
|
||||
'revision_log_message' => NULL,
|
||||
// @todo Attempt to remove this in https://www.drupal.org/project/drupal/issues/2933518.
|
||||
'revision_translation_affected' => TRUE,
|
||||
'drupal_internal__mid' => 1,
|
||||
'drupal_internal__vid' => 1,
|
||||
],
|
||||
'relationships' => [
|
||||
'field_media_file' => [
|
||||
'data' => [
|
||||
'id' => $file->uuid(),
|
||||
'meta' => [
|
||||
'description' => NULL,
|
||||
'display' => NULL,
|
||||
'drupal_internal__target_id' => (int) $file->id(),
|
||||
],
|
||||
'type' => 'file--file',
|
||||
],
|
||||
'links' => [
|
||||
'related' => [
|
||||
'href' => $base_url->toString() . '/field_media_file' . $version_query_string,
|
||||
],
|
||||
'self' => [
|
||||
'href' => $base_url->toString() . '/relationships/field_media_file' . $version_query_string,
|
||||
],
|
||||
],
|
||||
],
|
||||
'thumbnail' => [
|
||||
'data' => [
|
||||
'id' => $thumbnail->uuid(),
|
||||
'meta' => [
|
||||
'alt' => '',
|
||||
'drupal_internal__target_id' => (int) $thumbnail->id(),
|
||||
'width' => 180,
|
||||
'height' => 180,
|
||||
'title' => NULL,
|
||||
],
|
||||
'type' => 'file--file',
|
||||
],
|
||||
'links' => [
|
||||
'related' => [
|
||||
'href' => $base_url->toString() . '/thumbnail' . $version_query_string,
|
||||
],
|
||||
'self' => [
|
||||
'href' => $base_url->toString() . '/relationships/thumbnail' . $version_query_string,
|
||||
],
|
||||
],
|
||||
],
|
||||
'bundle' => [
|
||||
'data' => [
|
||||
'id' => MediaType::load('camelids')->uuid(),
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => 'camelids',
|
||||
],
|
||||
'type' => 'media_type--media_type',
|
||||
],
|
||||
'links' => [
|
||||
'related' => [
|
||||
'href' => $base_url->toString() . '/bundle' . $version_query_string,
|
||||
],
|
||||
'self' => [
|
||||
'href' => $base_url->toString() . '/relationships/bundle' . $version_query_string,
|
||||
],
|
||||
],
|
||||
],
|
||||
'uid' => [
|
||||
'data' => [
|
||||
'id' => $author->uuid(),
|
||||
'type' => 'user--user',
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => (int) $author->id(),
|
||||
],
|
||||
],
|
||||
'links' => [
|
||||
'related' => [
|
||||
'href' => $base_url->toString() . '/uid' . $version_query_string,
|
||||
],
|
||||
'self' => [
|
||||
'href' => $base_url->toString() . '/relationships/uid' . $version_query_string,
|
||||
],
|
||||
],
|
||||
],
|
||||
'revision_user' => [
|
||||
'data' => [
|
||||
'id' => $author->uuid(),
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => (int) $author->id(),
|
||||
],
|
||||
'type' => 'user--user',
|
||||
],
|
||||
'links' => [
|
||||
'related' => [
|
||||
'href' => $base_url->toString() . '/revision_user' . $version_query_string,
|
||||
],
|
||||
'self' => [
|
||||
'href' => $base_url->toString() . '/relationships/revision_user' . $version_query_string,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
$file = File::load(2);
|
||||
return [
|
||||
'data' => [
|
||||
'type' => 'media--camelids',
|
||||
'attributes' => [
|
||||
'name' => 'Drama llama',
|
||||
],
|
||||
'relationships' => [
|
||||
'field_media_file' => [
|
||||
'data' => [
|
||||
'id' => $file->uuid(),
|
||||
'meta' => [
|
||||
'description' => 'This file is better!',
|
||||
'display' => NULL,
|
||||
'drupal_internal__target_id' => (int) $file->id(),
|
||||
],
|
||||
'type' => 'file--file',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessMessage($method) {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
return "The 'view media' permission is required when the media item is published.";
|
||||
|
||||
case 'POST':
|
||||
return "The following permissions are required: 'administer media' OR 'create media' OR 'create camelids media'.";
|
||||
|
||||
case 'PATCH':
|
||||
return "The following permissions are required: 'update any media' OR 'update own media' OR 'camelids: edit any media' OR 'camelids: edit own media'.";
|
||||
|
||||
case 'DELETE':
|
||||
return "The following permissions are required: 'delete any media' OR 'delete own media' OR 'camelids: delete any media' OR 'camelids: delete own media'.";
|
||||
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getEditorialPermissions(): array {
|
||||
return array_merge(parent::getEditorialPermissions(), ['view any unpublished content']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessCacheability() {
|
||||
// @see \Drupal\media\MediaAccessControlHandler::checkAccess()
|
||||
return parent::getExpectedUnauthorizedAccessCacheability()
|
||||
->addCacheTags(['media:1']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function doTestPostIndividual(): void {
|
||||
// @todo Mimic \Drupal\Tests\rest\Functional\EntityResource\Media\MediaResourceTestBase::testPost()
|
||||
// @todo Later, use https://www.drupal.org/project/drupal/issues/2958554 to upload files rather than the REST module.
|
||||
parent::doTestPostIndividual();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedGetRelationshipDocumentData($relationship_field_name, ?EntityInterface $entity = NULL) {
|
||||
$data = parent::getExpectedGetRelationshipDocumentData($relationship_field_name, $entity);
|
||||
switch ($relationship_field_name) {
|
||||
case 'thumbnail':
|
||||
$data['meta'] = [
|
||||
'alt' => '',
|
||||
'width' => 180,
|
||||
'height' => 180,
|
||||
'title' => NULL,
|
||||
] + $data['meta'];
|
||||
return $data;
|
||||
|
||||
case 'field_media_file':
|
||||
$data['meta'] = [
|
||||
'description' => NULL,
|
||||
'display' => NULL,
|
||||
] + $data['meta'];
|
||||
return $data;
|
||||
|
||||
default:
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @todo Remove this in https://www.drupal.org/node/2824851.
|
||||
*/
|
||||
protected function doTestRelationshipMutation(array $request_options): void {
|
||||
$this->grantPermissionsToTestedRole(['access content']);
|
||||
parent::doTestRelationshipMutation($request_options);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function testCollectionFilterAccess(): void {
|
||||
$this->doTestCollectionFilterAccessForPublishableEntities('name', 'view media', 'administer media');
|
||||
}
|
||||
|
||||
}
|
||||
119
web/core/modules/jsonapi/tests/src/Functional/MediaTypeTest.php
Normal file
119
web/core/modules/jsonapi/tests/src/Functional/MediaTypeTest.php
Normal file
@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\media\Entity\MediaType;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "MediaType" config entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class MediaTypeTest extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['media'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'media_type';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'media_type--media_type';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\media\MediaTypeInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['administer media types']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
// Create a "Camelids" media type.
|
||||
$camelids = MediaType::create([
|
||||
'label' => 'Camelids',
|
||||
'id' => 'camelids',
|
||||
'description' => 'Camelids are large, strictly herbivorous animals with slender necks and long legs.',
|
||||
'source' => 'file',
|
||||
]);
|
||||
|
||||
$camelids->save();
|
||||
|
||||
return $camelids;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/media_type/media_type/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'media_type--media_type',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'dependencies' => [],
|
||||
'description' => 'Camelids are large, strictly herbivorous animals with slender necks and long legs.',
|
||||
'field_map' => [],
|
||||
'label' => 'Camelids',
|
||||
'langcode' => 'en',
|
||||
'new_revision' => FALSE,
|
||||
'queue_thumbnail_downloads' => FALSE,
|
||||
'source' => 'file',
|
||||
'source_configuration' => [
|
||||
'source_field' => '',
|
||||
],
|
||||
'status' => TRUE,
|
||||
'drupal_internal__id' => 'camelids',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
// @todo Update in https://www.drupal.org/node/2300677.
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,246 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\menu_link_content\Entity\MenuLinkContent;
|
||||
use Drupal\Tests\jsonapi\Traits\CommonCollectionFilterAccessTestPatternsTrait;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "MenuLinkContent" content entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class MenuLinkContentTest extends ResourceTestBase {
|
||||
|
||||
use CommonCollectionFilterAccessTestPatternsTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['menu_link_content'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'menu_link_content';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'menu_link_content--menu_link_content';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeIsVersionable = TRUE;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\menu_link_content\MenuLinkContentInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $patchProtectedFieldNames = [
|
||||
'changed' => NULL,
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['administer menu']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
$menu_link = MenuLinkContent::create([
|
||||
'id' => 'llama',
|
||||
'title' => 'Llama Gabilondo',
|
||||
'description' => 'Llama Gabilondo',
|
||||
'link' => 'https://nl.wikipedia.org/wiki/Llama',
|
||||
'weight' => 0,
|
||||
'menu_name' => 'main',
|
||||
]);
|
||||
$menu_link->save();
|
||||
|
||||
return $menu_link;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$base_url = Url::fromUri('base:/jsonapi/menu_link_content/menu_link_content/' . $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' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $base_url->toString()],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'menu_link_content--menu_link_content',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url->toString()],
|
||||
],
|
||||
'attributes' => [
|
||||
'bundle' => 'menu_link_content',
|
||||
'link' => [
|
||||
'uri' => 'https://nl.wikipedia.org/wiki/Llama',
|
||||
'title' => NULL,
|
||||
'options' => [],
|
||||
],
|
||||
'changed' => (new \DateTime())->setTimestamp($this->entity->getChangedTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
|
||||
'default_langcode' => TRUE,
|
||||
'description' => 'Llama Gabilondo',
|
||||
'enabled' => TRUE,
|
||||
'expanded' => FALSE,
|
||||
'external' => FALSE,
|
||||
'langcode' => 'en',
|
||||
'menu_name' => 'main',
|
||||
'parent' => NULL,
|
||||
'rediscover' => FALSE,
|
||||
'title' => 'Llama Gabilondo',
|
||||
'weight' => 0,
|
||||
'drupal_internal__id' => 1,
|
||||
'drupal_internal__revision_id' => 1,
|
||||
'revision_created' => (new \DateTime())->setTimestamp((int) $this->entity->getRevisionCreationTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
|
||||
'revision_log_message' => NULL,
|
||||
// @todo Attempt to remove this in https://www.drupal.org/project/drupal/issues/2933518.
|
||||
'revision_translation_affected' => TRUE,
|
||||
],
|
||||
'relationships' => [
|
||||
'revision_user' => [
|
||||
'data' => NULL,
|
||||
'links' => [
|
||||
'related' => [
|
||||
'href' => $base_url->toString() . '/revision_user' . $version_query_string,
|
||||
],
|
||||
'self' => [
|
||||
'href' => $base_url->toString() . '/relationships/revision_user' . $version_query_string,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
return [
|
||||
'data' => [
|
||||
'type' => 'menu_link_content--menu_link_content',
|
||||
'attributes' => [
|
||||
'title' => 'Drama llama',
|
||||
'link' => [
|
||||
'uri' => 'http://www.urbandictionary.com/define.php?term=drama%20llama',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessMessage($method) {
|
||||
switch ($method) {
|
||||
case 'DELETE':
|
||||
return "The 'administer menu' permission is required.";
|
||||
|
||||
default:
|
||||
return parent::getExpectedUnauthorizedAccessMessage($method);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function testCollectionFilterAccess(): void {
|
||||
$this->doTestCollectionFilterAccessBasedOnPermissions('title', 'administer menu');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests requests using a serialized field item property.
|
||||
*
|
||||
* @see https://security.drupal.org/node/161923
|
||||
*/
|
||||
public function testLinkOptionsSerialization(): void {
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
|
||||
$document = $this->getPostDocument();
|
||||
$document['data']['attributes']['link']['options'] = "O:44:\"Symfony\\Component\\Process\\Pipes\\WindowsPipes\":8:{s:51:\"\\Symfony\\Component\\Process\\Pipes\\WindowsPipes\0files\";a:1:{i:0;s:3:\"foo\";}s:57:\"\0Symfony\\Component\\Process\\Pipes\\WindowsPipes\0fileHandles\";a:0:{}s:55:\"\0Symfony\\Component\\Process\\Pipes\\WindowsPipes\0readBytes\";a:2:{i:1;i:0;i:2;i:0;}s:59:\"\0Symfony\\Component\\Process\\Pipes\\WindowsPipes\0disableOutput\";b:0;s:5:\"pipes\";a:0:{}s:58:\"\0Symfony\\Component\\Process\\Pipes\\AbstractPipes\0inputBuffer\";s:0:\"\";s:52:\"\0Symfony\\Component\\Process\\Pipes\\AbstractPipes\0input\";N;s:54:\"\0Symfony\\Component\\Process\\Pipes\\AbstractPipes\0blocked\";b:1;}";
|
||||
$url = Url::fromRoute(sprintf('jsonapi.%s.collection.post', static::$resourceTypeName));
|
||||
$request_options = [];
|
||||
$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($document);
|
||||
$request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions());
|
||||
|
||||
// Ensure 403 when unauthorized.
|
||||
$response = $this->request('POST', $url, $request_options);
|
||||
$reason = $this->getExpectedUnauthorizedAccessMessage('POST');
|
||||
$this->assertResourceErrorResponse(403, (string) $reason, $url, $response);
|
||||
|
||||
$this->setUpAuthorization('POST');
|
||||
|
||||
// Ensure that an exception is thrown.
|
||||
$response = $this->request('POST', $url, $request_options);
|
||||
$this->assertResourceErrorResponse(500, (string) 'The generic FieldItemNormalizer cannot denormalize string values for "options" properties of the "link" field (field item class: Drupal\link\Plugin\Field\FieldType\LinkItem).', $url, $response);
|
||||
|
||||
// Create a menu link content entity without the serialized property.
|
||||
unset($document['data']['attributes']['link']['options']);
|
||||
$request_options[RequestOptions::BODY] = Json::encode($document);
|
||||
$response = $this->request('POST', $url, $request_options);
|
||||
$document = $this->getDocumentFromResponse($response);
|
||||
$internal_id = $document['data']['attributes']['drupal_internal__id'];
|
||||
|
||||
// Load the created menu item and add link options to it.
|
||||
$menu_link = MenuLinkContent::load($internal_id);
|
||||
$menu_link->get('link')->first()->set('options', ['fragment' => 'test']);
|
||||
$menu_link->save();
|
||||
|
||||
// Fetch the link.
|
||||
unset($request_options[RequestOptions::BODY]);
|
||||
$url = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), ['entity' => $document['data']['id']]);
|
||||
$response = $this->request('GET', $url, $request_options);
|
||||
$response_body = (string) $response->getBody();
|
||||
|
||||
// Ensure that the entity can be updated using a response document.
|
||||
$request_options[RequestOptions::BODY] = $response_body;
|
||||
$response = $this->request('PATCH', $url, $request_options);
|
||||
$this->assertResourceResponse(200, Json::decode($response_body), $response);
|
||||
}
|
||||
|
||||
}
|
||||
115
web/core/modules/jsonapi/tests/src/Functional/MenuTest.php
Normal file
115
web/core/modules/jsonapi/tests/src/Functional/MenuTest.php
Normal file
@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\system\Entity\Menu;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "Menu" config entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class MenuTest extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'menu';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'menu--menu';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $anonymousUsersCanViewLabels = TRUE;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\system\MenuInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['administer menu']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
$menu = Menu::create([
|
||||
'id' => 'menu',
|
||||
'label' => 'Menu',
|
||||
'description' => 'Menu',
|
||||
]);
|
||||
$menu->save();
|
||||
|
||||
return $menu;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/menu/menu/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'menu--menu',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'dependencies' => [],
|
||||
'description' => 'Menu',
|
||||
'label' => 'Menu',
|
||||
'langcode' => 'en',
|
||||
'locked' => FALSE,
|
||||
'status' => TRUE,
|
||||
'drupal_internal__id' => 'menu',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
// @todo Update in https://www.drupal.org/node/2300677.
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
537
web/core/modules/jsonapi/tests/src/Functional/NodeTest.php
Normal file
537
web/core/modules/jsonapi/tests/src/Functional/NodeTest.php
Normal file
@ -0,0 +1,537 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\jsonapi\Normalizer\HttpExceptionNormalizer;
|
||||
use Drupal\jsonapi\Normalizer\Value\CacheableNormalization;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\Tests\jsonapi\Traits\CommonCollectionFilterAccessTestPatternsTrait;
|
||||
use Drupal\Tests\WaitTerminateTestTrait;
|
||||
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;
|
||||
use WaitTerminateTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['node', 'path'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@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): void {
|
||||
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): void {
|
||||
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(): array {
|
||||
$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' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'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_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(),
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => 'camelids',
|
||||
],
|
||||
'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(),
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => (int) $author->id(),
|
||||
],
|
||||
'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(),
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => (int) $author->id(),
|
||||
],
|
||||
'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(): array {
|
||||
return [
|
||||
'data' => [
|
||||
'type' => 'node--camelids',
|
||||
'attributes' => [
|
||||
'title' => 'Drama llama',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessMessage($method): string {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
case 'POST':
|
||||
case 'PATCH':
|
||||
case 'DELETE':
|
||||
return "The 'access content' permission is required.";
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(): void {
|
||||
$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/drupal/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 = $this->getDocumentFromResponse($response);
|
||||
|
||||
// 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);
|
||||
$updated_normalization = $this->getDocumentFromResponse($response);
|
||||
$this->assertResourceResponse(200, FALSE, $response);
|
||||
$this->assertSame($normalization['data']['attributes']['path']['alias'], $updated_normalization['data']['attributes']['path']['alias']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function testGetIndividual(): void {
|
||||
// Cacheable normalizations are written after the response is flushed to
|
||||
// the client. We use WaitTerminateTestTrait to wait for Drupal to perform
|
||||
// its termination work before continuing.
|
||||
$this->setWaitForTerminate();
|
||||
|
||||
parent::testGetIndividual();
|
||||
|
||||
$this->assertCacheableNormalizations();
|
||||
// Unpublish node.
|
||||
$this->entity->setUnpublished()->save();
|
||||
|
||||
// @todo Remove line below in favor of commented line in https://www.drupal.org/project/drupal/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);
|
||||
$this->assertResourceErrorResponse(
|
||||
403,
|
||||
'The current user is not allowed to GET the selected resource.',
|
||||
$url,
|
||||
$response,
|
||||
'/data',
|
||||
['4xx-response', 'http_response', 'node:1'],
|
||||
['url.query_args', 'url.site', 'user.permissions'],
|
||||
'UNCACHEABLE (request policy)',
|
||||
TRUE
|
||||
);
|
||||
|
||||
// 200 after granting permission.
|
||||
$this->grantPermissionsToTestedRole(['view own unpublished content']);
|
||||
$response = $this->request('GET', $url, $request_options);
|
||||
$this->assertResourceResponse(200, FALSE, $response, $this->getExpectedCacheTags(), $this->getExpectedCacheContexts(), 'UNCACHEABLE (request policy)', TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that normalizations are cached in an incremental way.
|
||||
*
|
||||
* @throws \Drupal\Core\Entity\EntityStorageException
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function assertCacheableNormalizations(): void {
|
||||
// Save the entity to invalidate caches.
|
||||
$this->entity->save();
|
||||
$uuid = $this->entity->uuid();
|
||||
$language = $this->entity->language()->getId();
|
||||
$cache = \Drupal::service('variation_cache.jsonapi_normalizations')->get(['node--camelids', $uuid, $language], new CacheableMetadata());
|
||||
// After saving the entity the normalization should not be cached.
|
||||
$this->assertFalse($cache);
|
||||
// @todo Remove line below in favor of commented line in https://www.drupal.org/project/drupal/issues/2878463.
|
||||
$url = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), ['entity' => $uuid]);
|
||||
// $url = $this->entity->toUrl('jsonapi');
|
||||
$request_options = $this->getAuthenticationRequestOptions();
|
||||
$request_options[RequestOptions::QUERY] = ['fields' => ['node--camelids' => 'title']];
|
||||
$this->request('GET', $url, $request_options);
|
||||
// Ensure the normalization cache is being incrementally built. After
|
||||
// requesting the title, only the title is in the cache.
|
||||
$this->assertNormalizedFieldsAreCached(['title']);
|
||||
$request_options[RequestOptions::QUERY] = ['fields' => ['node--camelids' => 'field_rest_test']];
|
||||
$this->request('GET', $url, $request_options);
|
||||
// After requesting an additional field, then that field is in the cache and
|
||||
// the old one is still there.
|
||||
$this->assertNormalizedFieldsAreCached(['title', 'field_rest_test']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the provided field names are the only fields in the cache.
|
||||
*
|
||||
* The normalization cache should only have these fields, which build up
|
||||
* across responses.
|
||||
*
|
||||
* @param string[] $field_names
|
||||
* The field names.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function assertNormalizedFieldsAreCached(array $field_names): void {
|
||||
$variation_cache = \Drupal::service('variation_cache.jsonapi_normalizations');
|
||||
|
||||
// Because we warm caches in different requests, we do not properly populate
|
||||
// the internal properties of our variation cache. Reset it.
|
||||
$variation_cache->reset();
|
||||
|
||||
$cache = $variation_cache->get(['node--camelids', $this->entity->uuid(), $this->entity->language()->getId()], new CacheableMetadata());
|
||||
$cached_fields = $cache->data['fields'];
|
||||
$this->assertSameSize($field_names, $cached_fields);
|
||||
array_walk($field_names, function ($field_name) use ($cached_fields) {
|
||||
$this->assertInstanceOf(
|
||||
CacheableNormalization::class,
|
||||
$cached_fields[$field_name]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedCacheContexts(?array $sparse_fieldset = NULL) {
|
||||
// \Drupal\Tests\jsonapi\Functional\ResourceTestBase::testRevisions()
|
||||
// loads different revisions via query parameters, we do our best
|
||||
// here to react to those directly, or indirectly.
|
||||
$cache_contexts = parent::getExpectedCacheContexts($sparse_fieldset);
|
||||
|
||||
// This is bubbled up by
|
||||
// \Drupal\node\NodeAccessControlHandler::checkAccess() directly.
|
||||
if ($this->entity->isPublished()) {
|
||||
return $cache_contexts;
|
||||
}
|
||||
if (!\Drupal::currentUser()->isAuthenticated()) {
|
||||
return Cache::mergeContexts($cache_contexts, ['user.roles:authenticated']);
|
||||
}
|
||||
if (\Drupal::currentUser()->hasPermission('view own unpublished content')) {
|
||||
return Cache::mergeContexts($cache_contexts, ['user']);
|
||||
}
|
||||
return $cache_contexts;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static function getIncludePermissions(): array {
|
||||
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(): void {
|
||||
$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(): void {
|
||||
$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 = $this->getDocumentFromResponse($response);
|
||||
$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 = $this->getDocumentFromResponse($response);
|
||||
$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 = $this->getDocumentFromResponse($response);
|
||||
$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->assertContains('user.node_grants:view', explode(' ', $response->getHeader('X-Drupal-Cache-Contexts')[0]));
|
||||
}
|
||||
|
||||
}
|
||||
122
web/core/modules/jsonapi/tests/src/Functional/NodeTypeTest.php
Normal file
122
web/core/modules/jsonapi/tests/src/Functional/NodeTypeTest.php
Normal file
@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "NodeType" config entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class NodeTypeTest extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['node'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'node_type';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'node_type--node_type';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\node\NodeTypeInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['administer content types', 'access content']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
// Create a "Camelids" node type.
|
||||
$camelids = NodeType::create([
|
||||
'name' => 'Camelids',
|
||||
'type' => 'camelids',
|
||||
'description' => 'Camelids are large, strictly herbivorous animals with slender necks and long legs.',
|
||||
]);
|
||||
|
||||
$camelids->save();
|
||||
|
||||
return $camelids;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/node_type/node_type/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'node_type--node_type',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'dependencies' => [],
|
||||
'description' => 'Camelids are large, strictly herbivorous animals with slender necks and long legs.',
|
||||
'display_submitted' => TRUE,
|
||||
'help' => NULL,
|
||||
'langcode' => 'en',
|
||||
'name' => 'Camelids',
|
||||
'new_revision' => TRUE,
|
||||
'preview_mode' => 1,
|
||||
'status' => TRUE,
|
||||
'drupal_internal__type' => 'camelids',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
// @todo Update in https://www.drupal.org/node/2300677.
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessMessage($method): string {
|
||||
return "The 'access content' permission is required.";
|
||||
}
|
||||
|
||||
}
|
||||
130
web/core/modules/jsonapi/tests/src/Functional/PathAliasTest.php
Normal file
130
web/core/modules/jsonapi/tests/src/Functional/PathAliasTest.php
Normal file
@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\path_alias\Entity\PathAlias;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "PathAlias" content entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
* @group path
|
||||
*/
|
||||
class PathAliasTest extends ResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['path'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'path_alias';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'path_alias--path_alias';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeIsVersionable = TRUE;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $patchProtectedFieldNames = [];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\user\RoleInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['administer url aliases']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
$path_alias = PathAlias::create([
|
||||
'alias' => '/frontpage1',
|
||||
'path' => '/<front>',
|
||||
'langcode' => 'en',
|
||||
]);
|
||||
$path_alias->save();
|
||||
return $path_alias;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$base_url = Url::fromUri('base:/jsonapi/path_alias/path_alias/' . $this->entity->uuid())->setAbsolute();
|
||||
$self_url = clone $base_url;
|
||||
$version_identifier = 'id:' . $this->entity->getRevisionId();
|
||||
$self_url = $self_url->setOption('query', ['resourceVersion' => $version_identifier]);
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $base_url->toString()],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => static::$resourceTypeName,
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url->toString()],
|
||||
],
|
||||
'attributes' => [
|
||||
'alias' => '/frontpage1',
|
||||
'path' => '/<front>',
|
||||
'langcode' => 'en',
|
||||
'status' => TRUE,
|
||||
'drupal_internal__id' => 1,
|
||||
'drupal_internal__revision_id' => 1,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
return [
|
||||
'data' => [
|
||||
'type' => static::$resourceTypeName,
|
||||
'attributes' => [
|
||||
'alias' => '/frontpage1',
|
||||
'path' => '/<front>',
|
||||
'langcode' => 'en',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,668 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Component\Utility\Crypt;
|
||||
use Drupal\Core\Access\AccessResultInterface;
|
||||
use Drupal\Core\Access\AccessResultReasonInterface;
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\RevisionableInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\jsonapi\CacheableResourceResponse;
|
||||
use Drupal\jsonapi\Normalizer\HttpExceptionNormalizer;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Utility methods for handling resource responses.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait ResourceResponseTestTrait {
|
||||
|
||||
/**
|
||||
* Merges individual responses into a collection response.
|
||||
*
|
||||
* Here, a collection response refers to a response with multiple resource
|
||||
* objects. Not necessarily to a response to a collection route. In both
|
||||
* cases, the document should indistinguishable.
|
||||
*
|
||||
* @param \Drupal\jsonapi\ResourceResponse[] $responses
|
||||
* An array or ResourceResponses to be merged.
|
||||
* @param string|null $self_link
|
||||
* The self link for the merged document if one should be set.
|
||||
* @param bool $is_multiple
|
||||
* Whether the responses are for a multiple cardinality field. This cannot
|
||||
* be deduced from the number of responses, because a multiple cardinality
|
||||
* field may have only one value.
|
||||
*
|
||||
* @return \Drupal\jsonapi\CacheableResourceResponse
|
||||
* The merged ResourceResponse.
|
||||
*/
|
||||
protected static function toCollectionResourceResponse(array $responses, $self_link, $is_multiple) {
|
||||
assert(count($responses) > 0);
|
||||
$merged_document = [];
|
||||
$merged_cacheability = new CacheableMetadata();
|
||||
foreach ($responses as $response) {
|
||||
$response_document = $response->getResponseData();
|
||||
// If any of the response documents had top-level errors, we should later
|
||||
// expect the merged document to have all errors as omitted links under
|
||||
// the 'meta.omitted' member.
|
||||
if (!empty($response_document['errors'])) {
|
||||
static::addOmittedObject($merged_document, static::errorsToOmittedObject($response_document['errors']));
|
||||
}
|
||||
if (!empty($response_document['meta']['omitted'])) {
|
||||
static::addOmittedObject($merged_document, $response_document['meta']['omitted']);
|
||||
}
|
||||
elseif (isset($response_document['data'])) {
|
||||
$response_data = $response_document['data'];
|
||||
if (!isset($merged_document['data'])) {
|
||||
$merged_document['data'] = static::isResourceIdentifier($response_data) && $is_multiple
|
||||
? [$response_data]
|
||||
: $response_data;
|
||||
}
|
||||
else {
|
||||
$response_resources = static::isResourceIdentifier($response_data)
|
||||
? [$response_data]
|
||||
: $response_data;
|
||||
foreach ($response_resources as $response_resource) {
|
||||
$merged_document['data'][] = $response_resource;
|
||||
}
|
||||
}
|
||||
}
|
||||
$merged_cacheability->addCacheableDependency($response->getCacheableMetadata());
|
||||
}
|
||||
$merged_document['jsonapi'] = [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
];
|
||||
// Until we can reasonably know what caused an error, we shouldn't include
|
||||
// 'self' links in error documents. For example, a 404 shouldn't have a
|
||||
// 'self' link because HATEOAS links shouldn't point to resources which do
|
||||
// not exist.
|
||||
if (isset($merged_document['errors'])) {
|
||||
unset($merged_document['links']);
|
||||
}
|
||||
else {
|
||||
if (!isset($merged_document['data'])) {
|
||||
$merged_document['data'] = $is_multiple ? [] : NULL;
|
||||
}
|
||||
$merged_document['links'] = [
|
||||
'self' => [
|
||||
'href' => $self_link,
|
||||
],
|
||||
];
|
||||
}
|
||||
// All collections should be 200, without regard for the status of the
|
||||
// individual resources in those collections, which means any '4xx-response'
|
||||
// cache tags on the individual responses should also be omitted.
|
||||
$merged_cacheability->setCacheTags(array_diff($merged_cacheability->getCacheTags(), ['4xx-response']));
|
||||
return (new CacheableResourceResponse($merged_document, 200))->addCacheableDependency($merged_cacheability);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an array of expected ResourceResponses for the given include paths.
|
||||
*
|
||||
* @param array $include_paths
|
||||
* The list of relationship include paths for which to get expected data.
|
||||
* @param array $request_options
|
||||
* Request options to apply.
|
||||
*
|
||||
* @return \Drupal\jsonapi\ResourceResponse
|
||||
* The expected ResourceResponse.
|
||||
*
|
||||
* @see \GuzzleHttp\ClientInterface::request()
|
||||
*/
|
||||
protected function getExpectedIncludedResourceResponse(array $include_paths, array $request_options) {
|
||||
$resource_type = $this->resourceType;
|
||||
$resource_data = array_reduce($include_paths, function ($data, $path) use ($request_options, $resource_type) {
|
||||
$field_names = explode('.', $path);
|
||||
/** @var \Drupal\Core\Entity\EntityInterface $entity */
|
||||
$entity = $this->entity;
|
||||
$collected_responses = [];
|
||||
foreach ($field_names as $public_field_name) {
|
||||
$resource_type = $this->container->get('jsonapi.resource_type.repository')->get($entity->getEntityTypeId(), $entity->bundle());
|
||||
$field_name = $resource_type->getInternalName($public_field_name);
|
||||
$field_access = static::entityFieldAccess($entity, $field_name, 'view', $this->account);
|
||||
if (!$field_access->isAllowed()) {
|
||||
if (!$entity->access('view') && $entity->access('view label') && $field_access instanceof AccessResultReasonInterface && empty($field_access->getReason())) {
|
||||
$field_access->setReason("The user only has authorization for the 'view label' operation.");
|
||||
}
|
||||
$via_link = Url::fromRoute(
|
||||
sprintf('jsonapi.%s.%s.related', $entity->getEntityTypeId() . '--' . $entity->bundle(), $public_field_name),
|
||||
['entity' => $entity->uuid()]
|
||||
);
|
||||
$collected_responses[] = static::getAccessDeniedResponse($entity, $field_access, $via_link, $field_name, 'The current user is not allowed to view this relationship.', $field_name);
|
||||
break;
|
||||
}
|
||||
if ($target_entity = $entity->{$field_name}->entity) {
|
||||
$target_access = static::entityAccess($target_entity, 'view', $this->account);
|
||||
if (!$target_access->isAllowed()) {
|
||||
$target_access = static::entityAccess($target_entity, 'view label', $this->account)->addCacheableDependency($target_access);
|
||||
}
|
||||
if (!$target_access->isAllowed()) {
|
||||
$resource_identifier = static::toResourceIdentifier($target_entity);
|
||||
if (!static::collectionHasResourceIdentifier($resource_identifier, $data['already_checked'])) {
|
||||
$data['already_checked'][] = $resource_identifier;
|
||||
$via_link = Url::fromRoute(
|
||||
sprintf('jsonapi.%s.individual', $resource_identifier['type']),
|
||||
['entity' => $resource_identifier['id']]
|
||||
);
|
||||
$collected_responses[] = static::getAccessDeniedResponse($entity, $target_access, $via_link, NULL, NULL, '/data');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
$psr_responses = $this->getResponses([static::getRelatedLink(static::toResourceIdentifier($entity), $public_field_name)], $request_options);
|
||||
$collected_responses[] = static::toCollectionResourceResponse(static::toResourceResponses($psr_responses), NULL, TRUE);
|
||||
$entity = $entity->{$field_name}->entity;
|
||||
}
|
||||
if (!empty($collected_responses)) {
|
||||
$data['responses'][$path] = static::toCollectionResourceResponse($collected_responses, NULL, TRUE);
|
||||
}
|
||||
return $data;
|
||||
}, ['responses' => [], 'already_checked' => []]);
|
||||
|
||||
$individual_document = $this->getExpectedDocument();
|
||||
|
||||
$expected_base_url = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), ['entity' => $this->entity->uuid()])->setAbsolute();
|
||||
$include_url = clone $expected_base_url;
|
||||
$query = ['include' => implode(',', $include_paths)];
|
||||
$include_url->setOption('query', $query);
|
||||
$individual_document['links']['self']['href'] = $include_url->toString();
|
||||
|
||||
// The test entity reference field should always be present.
|
||||
if (!isset($individual_document['data']['relationships']['field_jsonapi_test_entity_ref'])) {
|
||||
if (static::$resourceTypeIsVersionable) {
|
||||
assert($this->entity instanceof RevisionableInterface);
|
||||
$version_identifier = 'id:' . $this->entity->getRevisionId();
|
||||
$version_query_string = '?resourceVersion=' . urlencode($version_identifier);
|
||||
}
|
||||
else {
|
||||
$version_query_string = '';
|
||||
}
|
||||
$individual_document['data']['relationships']['field_jsonapi_test_entity_ref'] = [
|
||||
'data' => [],
|
||||
'links' => [
|
||||
'related' => [
|
||||
'href' => $expected_base_url->toString() . '/field_jsonapi_test_entity_ref' . $version_query_string,
|
||||
],
|
||||
'self' => [
|
||||
'href' => $expected_base_url->toString() . '/relationships/field_jsonapi_test_entity_ref' . $version_query_string,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$basic_cacheability = (new CacheableMetadata())
|
||||
->addCacheTags($this->getExpectedCacheTags())
|
||||
->addCacheContexts($this->getExpectedCacheContexts());
|
||||
return static::decorateExpectedResponseForIncludedFields(new CacheableResourceResponse($individual_document), $resource_data['responses'])
|
||||
->addCacheableDependency($basic_cacheability);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps an array of PSR responses to JSON:API ResourceResponses.
|
||||
*
|
||||
* @param \Psr\Http\Message\ResponseInterface[] $responses
|
||||
* The PSR responses to be mapped.
|
||||
*
|
||||
* @return \Drupal\jsonapi\ResourceResponse[]
|
||||
* The ResourceResponses.
|
||||
*/
|
||||
protected static function toResourceResponses(array $responses): array {
|
||||
return array_map([self::class, 'toResourceResponse'], $responses);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a response object to a JSON:API ResourceResponse.
|
||||
*
|
||||
* This helper can be used to ease comparing, recording and merging
|
||||
* cacheable responses and to have easier access to the JSON:API document as
|
||||
* an array instead of a string.
|
||||
*
|
||||
* @param \Psr\Http\Message\ResponseInterface $response
|
||||
* A PSR response to be mapped.
|
||||
*
|
||||
* @return \Drupal\jsonapi\CacheableResourceResponse
|
||||
* The ResourceResponse.
|
||||
*/
|
||||
protected static function toResourceResponse(ResponseInterface $response) {
|
||||
$cacheability = new CacheableMetadata();
|
||||
if ($cache_tags = $response->getHeader('X-Drupal-Cache-Tags')) {
|
||||
$cacheability->addCacheTags(explode(' ', $cache_tags[0]));
|
||||
}
|
||||
if (!empty($response->getHeaderLine('X-Drupal-Cache-Contexts'))) {
|
||||
$cacheability->addCacheContexts(explode(' ', $response->getHeader('X-Drupal-Cache-Contexts')[0]));
|
||||
}
|
||||
if ($dynamic_cache = $response->getHeader('X-Drupal-Dynamic-Cache')) {
|
||||
$cacheability->setCacheMaxAge((str_contains($dynamic_cache[0], 'UNCACHEABLE') && $response->getStatusCode() < 400) ? 0 : Cache::PERMANENT);
|
||||
}
|
||||
$related_document = Json::decode($response->getBody());
|
||||
$resource_response = new CacheableResourceResponse($related_document, $response->getStatusCode());
|
||||
return $resource_response->addCacheableDependency($cacheability);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps an entity to a resource identifier.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity to map to a resource identifier.
|
||||
*
|
||||
* @return array
|
||||
* A resource identifier for the given entity.
|
||||
*/
|
||||
protected static function toResourceIdentifier(EntityInterface $entity): array {
|
||||
return [
|
||||
'type' => $entity->getEntityTypeId() . '--' . $entity->bundle(),
|
||||
'id' => $entity->uuid(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given array is a resource identifier.
|
||||
*
|
||||
* @param array $data
|
||||
* An array to check.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the array has a type and ID, FALSE otherwise.
|
||||
*/
|
||||
protected static function isResourceIdentifier(array $data): bool {
|
||||
return array_key_exists('type', $data) && array_key_exists('id', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts a collection of resources or resource identifiers.
|
||||
*
|
||||
* This is useful for asserting collections or resources where order cannot
|
||||
* be known in advance.
|
||||
*
|
||||
* @param array $resources
|
||||
* The resource or resource identifier.
|
||||
*/
|
||||
protected static function sortResourceCollection(array &$resources) {
|
||||
usort($resources, function ($a, $b) {
|
||||
return strcmp("{$a['type']}:{$a['id']}", "{$b['type']}:{$b['id']}");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a given resource exists in a list of resources.
|
||||
*
|
||||
* @param array $needle
|
||||
* The resource or resource identifier.
|
||||
* @param array $haystack
|
||||
* The list of resources or resource identifiers to search.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the needle exists is present in the haystack, FALSE otherwise.
|
||||
*/
|
||||
protected static function collectionHasResourceIdentifier(array $needle, array $haystack): bool {
|
||||
foreach ($haystack as $resource) {
|
||||
if ($resource['type'] == $needle['type'] && $resource['id'] == $needle['id']) {
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns a list of relationship field names into an array of link paths.
|
||||
*
|
||||
* @param array $relationship_field_names
|
||||
* The relationships field names for which to build link paths.
|
||||
* @param string $type
|
||||
* The type of link to get. Either 'relationship' or 'related'.
|
||||
*
|
||||
* @return array
|
||||
* An array of link paths, keyed by relationship field name.
|
||||
*/
|
||||
protected static function getLinkPaths(array $relationship_field_names, $type) {
|
||||
assert($type === 'relationship' || $type === 'related');
|
||||
return array_reduce($relationship_field_names, function ($link_paths, $relationship_field_name) use ($type) {
|
||||
$tail = $type === 'relationship' ? 'self' : $type;
|
||||
$link_paths[$relationship_field_name] = "data.relationships.$relationship_field_name.links.$tail.href";
|
||||
return $link_paths;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts links from a document using a list of relationship field names.
|
||||
*
|
||||
* @param array $link_paths
|
||||
* A list of paths to link values keyed by a name.
|
||||
* @param array $document
|
||||
* A JSON:API document.
|
||||
*
|
||||
* @return array
|
||||
* The extracted links, keyed by the original associated key name.
|
||||
*/
|
||||
protected static function extractLinks(array $link_paths, array $document): array {
|
||||
return array_map(function ($link_path) use ($document) {
|
||||
$link = array_reduce(
|
||||
explode('.', $link_path),
|
||||
'array_column',
|
||||
[$document]
|
||||
);
|
||||
return ($link) ? reset($link) : NULL;
|
||||
}, $link_paths);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates individual resource links for a list of resource identifiers.
|
||||
*
|
||||
* @param array $resource_identifiers
|
||||
* A list of resource identifiers for which to create links.
|
||||
*
|
||||
* @return string[]
|
||||
* The resource links.
|
||||
*/
|
||||
protected static function getResourceLinks(array $resource_identifiers): array {
|
||||
return array_map([static::class, 'getResourceLink'], $resource_identifiers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an individual resource link for a given resource identifier.
|
||||
*
|
||||
* @param array $resource_identifier
|
||||
* A resource identifier for which to create a link.
|
||||
*
|
||||
* @return string
|
||||
* The resource link.
|
||||
*/
|
||||
protected static function getResourceLink(array $resource_identifier) {
|
||||
assert(static::isResourceIdentifier($resource_identifier));
|
||||
$resource_type = $resource_identifier['type'];
|
||||
$resource_id = $resource_identifier['id'];
|
||||
$url = Url::fromRoute(sprintf('jsonapi.%s.individual', $resource_type), ['entity' => $resource_id]);
|
||||
return $url->setAbsolute()->toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a relationship link for a given resource identifier and field.
|
||||
*
|
||||
* @param array $resource_identifier
|
||||
* A resource identifier for which to create a link.
|
||||
* @param string $relationship_field_name
|
||||
* The relationship field for which to create a link.
|
||||
*
|
||||
* @return string
|
||||
* The relationship link.
|
||||
*/
|
||||
protected static function getRelationshipLink(array $resource_identifier, $relationship_field_name): string {
|
||||
return static::getResourceLink($resource_identifier) . "/relationships/$relationship_field_name";
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a related resource link for a given resource identifier and field.
|
||||
*
|
||||
* @param array $resource_identifier
|
||||
* A resource identifier for which to create a link.
|
||||
* @param string $relationship_field_name
|
||||
* The relationship field for which to create a link.
|
||||
*
|
||||
* @return string
|
||||
* The related resource link.
|
||||
*/
|
||||
protected static function getRelatedLink(array $resource_identifier, $relationship_field_name): string {
|
||||
return static::getResourceLink($resource_identifier) . "/$relationship_field_name";
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an array of related responses for the given field names.
|
||||
*
|
||||
* @param array $relationship_field_names
|
||||
* The list of relationship field names for which to get responses.
|
||||
* @param array $request_options
|
||||
* Request options to apply.
|
||||
* @param \Drupal\Core\Entity\EntityInterface|null $entity
|
||||
* (optional) The entity for which to get expected related responses.
|
||||
*
|
||||
* @return array
|
||||
* The related responses, keyed by relationship field names.
|
||||
*
|
||||
* @see \GuzzleHttp\ClientInterface::request()
|
||||
*/
|
||||
protected function getRelatedResponses(array $relationship_field_names, array $request_options, ?EntityInterface $entity = NULL) {
|
||||
$entity = $entity ?: $this->entity;
|
||||
$links = array_map(function ($relationship_field_name) use ($entity) {
|
||||
return static::getRelatedLink(static::toResourceIdentifier($entity), $relationship_field_name);
|
||||
}, array_combine($relationship_field_names, $relationship_field_names));
|
||||
return $this->getResponses($links, $request_options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an array of relationship responses for the given field names.
|
||||
*
|
||||
* @param array $relationship_field_names
|
||||
* The list of relationship field names for which to get responses.
|
||||
* @param array $request_options
|
||||
* Request options to apply.
|
||||
*
|
||||
* @return array
|
||||
* The relationship responses, keyed by relationship field names.
|
||||
*
|
||||
* @see \GuzzleHttp\ClientInterface::request()
|
||||
*/
|
||||
protected function getRelationshipResponses(array $relationship_field_names, array $request_options) {
|
||||
$links = array_map(function ($relationship_field_name) {
|
||||
return static::getRelationshipLink(static::toResourceIdentifier($this->entity), $relationship_field_name);
|
||||
}, array_combine($relationship_field_names, $relationship_field_names));
|
||||
return $this->getResponses($links, $request_options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets responses from an array of links.
|
||||
*
|
||||
* @param array $links
|
||||
* A keyed array of links.
|
||||
* @param array $request_options
|
||||
* Request options to apply.
|
||||
*
|
||||
* @return array
|
||||
* The fetched array of responses, keys are preserved.
|
||||
*
|
||||
* @see \GuzzleHttp\ClientInterface::request()
|
||||
*/
|
||||
protected function getResponses(array $links, array $request_options) {
|
||||
return array_reduce(array_keys($links), function ($related_responses, $key) use ($links, $request_options) {
|
||||
$related_responses[$key] = $this->request('GET', Url::fromUri($links[$key]), $request_options);
|
||||
return $related_responses;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a generic forbidden response.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity for which to generate the forbidden response.
|
||||
* @param \Drupal\Core\Access\AccessResultInterface $access
|
||||
* The denied AccessResult. This can carry a reason and cacheability data.
|
||||
* @param \Drupal\Core\Url $via_link
|
||||
* The source URL for the errors of the response.
|
||||
* @param string|null $relationship_field_name
|
||||
* (optional) The field name to which the forbidden result applies. Useful
|
||||
* for testing related/relationship routes and includes.
|
||||
* @param string|null $detail
|
||||
* (optional) Details for the JSON:API error object.
|
||||
* @param string|bool|null $pointer
|
||||
* (optional) Document pointer for the JSON:API error object. FALSE to omit
|
||||
* the pointer.
|
||||
*
|
||||
* @return \Drupal\jsonapi\CacheableResourceResponse
|
||||
* The forbidden ResourceResponse.
|
||||
*/
|
||||
protected static function getAccessDeniedResponse(EntityInterface $entity, AccessResultInterface $access, Url $via_link, $relationship_field_name = NULL, $detail = NULL, $pointer = NULL) {
|
||||
$detail = ($detail) ? $detail : 'The current user is not allowed to GET the selected resource.';
|
||||
if ($access instanceof AccessResultReasonInterface && ($reason = $access->getReason())) {
|
||||
$detail .= ' ' . $reason;
|
||||
}
|
||||
$error = [
|
||||
'status' => '403',
|
||||
'title' => 'Forbidden',
|
||||
'detail' => $detail,
|
||||
'links' => [
|
||||
'info' => ['href' => HttpExceptionNormalizer::getInfoUrl(403)],
|
||||
],
|
||||
];
|
||||
if ($pointer || $pointer !== FALSE && $relationship_field_name) {
|
||||
$error['source']['pointer'] = ($pointer) ? $pointer : $relationship_field_name;
|
||||
}
|
||||
if ($via_link) {
|
||||
$error['links']['via']['href'] = $via_link->setAbsolute()->toString();
|
||||
}
|
||||
|
||||
return (new CacheableResourceResponse([
|
||||
'jsonapi' => static::$jsonApiMember,
|
||||
'errors' => [$error],
|
||||
], 403))
|
||||
->addCacheableDependency((new CacheableMetadata())->addCacheTags(['4xx-response', 'http_response'])->addCacheContexts(['url.query_args', 'url.site']))
|
||||
->addCacheableDependency($access);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a generic empty collection response.
|
||||
*
|
||||
* @param int $cardinality
|
||||
* The cardinality of the resource collection. 1 for a to-one related
|
||||
* resource collection; -1 for an unlimited cardinality.
|
||||
* @param string $self_link
|
||||
* The self link for collection ResourceResponse.
|
||||
*
|
||||
* @return \Drupal\jsonapi\CacheableResourceResponse
|
||||
* The empty collection ResourceResponse.
|
||||
*/
|
||||
protected function getEmptyCollectionResponse($cardinality, $self_link) {
|
||||
// If the entity type is revisionable, add a resource version cache context.
|
||||
$cache_contexts = Cache::mergeContexts([
|
||||
// Cache contexts for JSON:API URL query parameters.
|
||||
'url.query_args',
|
||||
// Drupal defaults.
|
||||
'url.site',
|
||||
], $this->entity->getEntityType()->isRevisionable() ? ['url.query_args:resourceVersion'] : []);
|
||||
$cacheability = (new CacheableMetadata())->addCacheContexts($cache_contexts)->addCacheTags(['http_response']);
|
||||
return (new CacheableResourceResponse([
|
||||
// Empty to-one relationships should be NULL and empty to-many
|
||||
// relationships should be an empty array.
|
||||
'data' => $cardinality === 1 ? NULL : [],
|
||||
'jsonapi' => static::$jsonApiMember,
|
||||
'links' => ['self' => ['href' => $self_link]],
|
||||
]))->addCacheableDependency($cacheability);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the omitted object to the document or merges it if one already exists.
|
||||
*
|
||||
* @param array $document
|
||||
* The JSON:API response document.
|
||||
* @param array $omitted
|
||||
* The omitted object.
|
||||
*/
|
||||
protected static function addOmittedObject(array &$document, array $omitted) {
|
||||
if (isset($document['meta']['omitted'])) {
|
||||
$document['meta']['omitted'] = static::mergeOmittedObjects($document['meta']['omitted'], $omitted);
|
||||
}
|
||||
else {
|
||||
$document['meta']['omitted'] = $omitted;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps error objects into an omitted object.
|
||||
*
|
||||
* @param array $errors
|
||||
* An array of error objects.
|
||||
*
|
||||
* @return array
|
||||
* A new omitted object.
|
||||
*/
|
||||
protected static function errorsToOmittedObject(array $errors): array {
|
||||
$omitted = [
|
||||
'detail' => 'Some resources have been omitted because of insufficient authorization.',
|
||||
'links' => [
|
||||
'help' => [
|
||||
'href' => 'https://www.drupal.org/docs/8/modules/json-api/filtering#filters-access-control',
|
||||
],
|
||||
],
|
||||
];
|
||||
foreach ($errors as $error) {
|
||||
$omitted['links']['item--' . substr(Crypt::hashBase64($error['links']['via']['href']), 0, 7)] = [
|
||||
'href' => $error['links']['via']['href'],
|
||||
'meta' => [
|
||||
'detail' => $error['detail'],
|
||||
'rel' => 'item',
|
||||
],
|
||||
];
|
||||
}
|
||||
return $omitted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the links of two omitted objects and returns a new omitted object.
|
||||
*
|
||||
* @param array $a
|
||||
* The first omitted object.
|
||||
* @param array $b
|
||||
* The second omitted object.
|
||||
*
|
||||
* @return mixed
|
||||
* A new, merged omitted object.
|
||||
*/
|
||||
protected static function mergeOmittedObjects(array $a, array $b) {
|
||||
$merged['detail'] = 'Some resources have been omitted because of insufficient authorization.';
|
||||
$merged['links']['help']['href'] = 'https://www.drupal.org/docs/8/modules/json-api/filtering#filters-access-control';
|
||||
$a_links = array_diff_key($a['links'], array_flip(['help']));
|
||||
$b_links = array_diff_key($b['links'], array_flip(['help']));
|
||||
foreach (array_merge(array_values($a_links), array_values($b_links)) as $link) {
|
||||
$merged['links'][$link['href'] . $link['meta']['detail']] = $link;
|
||||
}
|
||||
static::resetOmittedLinkKeys($merged);
|
||||
return $merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts an omitted link object array by href.
|
||||
*
|
||||
* @param array $omitted
|
||||
* An array of JSON:API omitted link objects.
|
||||
*/
|
||||
protected static function sortOmittedLinks(array &$omitted) {
|
||||
$help = $omitted['links']['help'];
|
||||
$links = array_diff_key($omitted['links'], array_flip(['help']));
|
||||
uasort($links, function ($a, $b) {
|
||||
return strcmp($a['href'], $b['href']);
|
||||
});
|
||||
$omitted['links'] = ['help' => $help] + $links;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets omitted link keys.
|
||||
*
|
||||
* Omitted link keys are a link relation type + a random string. This string
|
||||
* is meaningless and only serves to differentiate link objects. Given that
|
||||
* these are random, we can't assert their value.
|
||||
*
|
||||
* @param array $omitted
|
||||
* An array of JSON:API omitted link objects.
|
||||
*/
|
||||
protected static function resetOmittedLinkKeys(array &$omitted) {
|
||||
$help = $omitted['links']['help'];
|
||||
$reindexed = [];
|
||||
$links = array_diff_key($omitted['links'], array_flip(['help']));
|
||||
foreach (array_values($links) as $index => $link) {
|
||||
$reindexed['item--' . $index] = $link;
|
||||
}
|
||||
$omitted['links'] = ['help' => $help] + $reindexed;
|
||||
}
|
||||
|
||||
}
|
||||
3602
web/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php
Normal file
3602
web/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\responsive_image\Entity\ResponsiveImageStyle;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "ResponsiveImageStyle" config entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class ResponsiveImageStyleTest extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['responsive_image'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'responsive_image_style';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'responsive_image_style--responsive_image_style';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\responsive_image\ResponsiveImageStyleInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['administer responsive images']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
// Create a "Camelids" responsive image style.
|
||||
$camelids = ResponsiveImageStyle::create([
|
||||
'id' => 'camelids',
|
||||
'label' => 'Camelids',
|
||||
]);
|
||||
$camelids->setBreakpointGroup('test_group');
|
||||
$camelids->setFallbackImageStyle('fallback');
|
||||
$camelids->addImageStyleMapping('test_breakpoint', '1x', [
|
||||
'image_mapping_type' => 'image_style',
|
||||
'image_mapping' => 'small',
|
||||
]);
|
||||
$camelids->addImageStyleMapping('test_breakpoint', '2x', [
|
||||
'image_mapping_type' => 'sizes',
|
||||
'image_mapping' => [
|
||||
'sizes' => '(min-width:700px) 700px, 100vw',
|
||||
'sizes_image_styles' => [
|
||||
'medium' => 'medium',
|
||||
'large' => 'large',
|
||||
],
|
||||
],
|
||||
]);
|
||||
$camelids->save();
|
||||
|
||||
return $camelids;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/responsive_image_style/responsive_image_style/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'responsive_image_style--responsive_image_style',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'breakpoint_group' => 'test_group',
|
||||
'dependencies' => [
|
||||
'config' => [
|
||||
'image.style.large',
|
||||
'image.style.medium',
|
||||
],
|
||||
],
|
||||
'fallback_image_style' => 'fallback',
|
||||
'image_style_mappings' => [
|
||||
0 => [
|
||||
'breakpoint_id' => 'test_breakpoint',
|
||||
'image_mapping' => 'small',
|
||||
'image_mapping_type' => 'image_style',
|
||||
'multiplier' => '1x',
|
||||
],
|
||||
1 => [
|
||||
'breakpoint_id' => 'test_breakpoint',
|
||||
'image_mapping' => [
|
||||
'sizes' => '(min-width:700px) 700px, 100vw',
|
||||
'sizes_image_styles' => [
|
||||
'large' => 'large',
|
||||
'medium' => 'medium',
|
||||
],
|
||||
],
|
||||
'image_mapping_type' => 'sizes',
|
||||
'multiplier' => '2x',
|
||||
],
|
||||
],
|
||||
'label' => 'Camelids',
|
||||
'langcode' => 'en',
|
||||
'status' => TRUE,
|
||||
'drupal_internal__id' => 'camelids',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
// @todo Update in https://www.drupal.org/node/2300677.
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\Tests\views\Functional\ViewTestBase;
|
||||
|
||||
/**
|
||||
* Ensures that the 'api_json' format is not supported by the REST module.
|
||||
*
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class RestExportJsonApiUnsupportedTest extends ViewTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $testViews = ['test_serializer_display_entity'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['jsonapi', 'rest_test_views', 'views_ui'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp($import_test_views = TRUE, $modules = ['rest_test_views']): void {
|
||||
parent::setUp($import_test_views, $modules);
|
||||
|
||||
$this->drupalLogin($this->drupalCreateUser(['administer views']));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that 'api_json' is not a RestExport format option.
|
||||
*/
|
||||
public function testFormatOptions(): void {
|
||||
$this->assertSame(['json' => 'serialization', 'xml' => 'serialization'], $this->container->getParameter('serializer.format_providers'));
|
||||
|
||||
$this->drupalGet('admin/structure/views/nojs/display/test_serializer_display_entity/rest_export_1/style_options');
|
||||
$this->assertSession()->fieldExists('style_options[formats][json]');
|
||||
$this->assertSession()->fieldExists('style_options[formats][xml]');
|
||||
$this->assertSession()->fieldNotExists('style_options[formats][api_json]');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
|
||||
use Drupal\Tests\rest\Functional\ResourceTestBase;
|
||||
|
||||
/**
|
||||
* Ensures that the 'api_json' format is not supported by the REST module.
|
||||
*
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class RestJsonApiUnsupportedTest extends ResourceTestBase {
|
||||
|
||||
use AnonResourceTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['jsonapi', 'node'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $format = 'api_json';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $mimeType = 'application/vnd.api+json';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceConfigId = 'entity.node';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
$this->grantPermissionsToTestedRole(['access content']);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \UnexpectedValueException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->config('system.logging')->set('error_level', ERROR_REPORTING_HIDE)->save();
|
||||
|
||||
// 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(0)
|
||||
->setPublished()
|
||||
->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploying a REST resource using api_json format results in 400 responses.
|
||||
*
|
||||
* @see \Drupal\jsonapi\EventSubscriber\JsonApiRequestValidator::validateQueryParams()
|
||||
*/
|
||||
public function testApiJsonNotSupportedInRest(): void {
|
||||
$this->assertSame(['json', 'xml'], $this->container->getParameter('serializer.formats'));
|
||||
|
||||
$this->provisionResource(['api_json'], []);
|
||||
$this->setUpAuthorization('GET');
|
||||
|
||||
$url = Node::load(1)->toUrl()
|
||||
->setOption('query', ['_format' => 'api_json']);
|
||||
$request_options = [];
|
||||
|
||||
$response = $this->request('GET', $url, $request_options);
|
||||
$this->assertResourceErrorResponse(
|
||||
400,
|
||||
FALSE,
|
||||
$response,
|
||||
['4xx-response', 'config:system.logging', 'config:user.role.anonymous', 'http_response', 'node:1'],
|
||||
['url.query_args', 'url.site', 'user.permissions'],
|
||||
'MISS',
|
||||
'MISS'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function assertNormalizationEdgeCases($method, Url $url, array $request_options): void {}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessMessage($method): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessCacheability() {
|
||||
return (new CacheableMetadata());
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\rest\Entity\RestResourceConfig;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "RestResourceConfig" config entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class RestResourceConfigTest extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['rest', 'dblog'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'rest_resource_config';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'rest_resource_config--rest_resource_config';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\rest\RestResourceConfigInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['administer rest resources']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
$rest_resource_config = RestResourceConfig::create([
|
||||
'id' => 'llama',
|
||||
'plugin_id' => 'dblog',
|
||||
'granularity' => 'method',
|
||||
'configuration' => [
|
||||
'GET' => [
|
||||
'supported_formats' => [
|
||||
'json',
|
||||
],
|
||||
'supported_auth' => [
|
||||
'cookie',
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
$rest_resource_config->save();
|
||||
|
||||
return $rest_resource_config;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/rest_resource_config/rest_resource_config/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'rest_resource_config--rest_resource_config',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'langcode' => 'en',
|
||||
'status' => TRUE,
|
||||
'dependencies' => [
|
||||
'module' => [
|
||||
'dblog',
|
||||
'serialization',
|
||||
'user',
|
||||
],
|
||||
],
|
||||
'plugin_id' => 'dblog',
|
||||
'granularity' => 'method',
|
||||
'configuration' => [
|
||||
'GET' => [
|
||||
'supported_formats' => [
|
||||
'json',
|
||||
],
|
||||
'supported_auth' => [
|
||||
'cookie',
|
||||
],
|
||||
],
|
||||
],
|
||||
'drupal_internal__id' => 'llama',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
// @todo Update in https://www.drupal.org/node/2300677.
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
110
web/core/modules/jsonapi/tests/src/Functional/RoleTest.php
Normal file
110
web/core/modules/jsonapi/tests/src/Functional/RoleTest.php
Normal file
@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\user\Entity\Role;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "Role" config entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class RoleTest extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['user'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'user_role';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'user_role--user_role';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\user\RoleInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['administer permissions']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
$role = Role::create([
|
||||
'id' => 'llama',
|
||||
'label' => 'Llama',
|
||||
]);
|
||||
$role->save();
|
||||
|
||||
return $role;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/user_role/user_role/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'user_role--user_role',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'weight' => 2,
|
||||
'langcode' => 'en',
|
||||
'status' => TRUE,
|
||||
'dependencies' => [],
|
||||
'label' => 'Llama',
|
||||
'is_admin' => FALSE,
|
||||
'permissions' => [],
|
||||
'drupal_internal__id' => 'llama',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
// @todo Update in https://www.drupal.org/node/2300677.
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
152
web/core/modules/jsonapi/tests/src/Functional/SearchPageTest.php
Normal file
152
web/core/modules/jsonapi/tests/src/Functional/SearchPageTest.php
Normal file
@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\search\Entity\SearchPage;
|
||||
|
||||
// cspell:ignore hinode
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "SearchPage" config entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class SearchPageTest extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['node', 'search'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'search_page';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'search_page--search_page';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\search\SearchPageInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
$this->grantPermissionsToTestedRole(['access content']);
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
case 'PATCH':
|
||||
case 'DELETE':
|
||||
$this->grantPermissionsToTestedRole(['administer search']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
$search_page = SearchPage::create([
|
||||
'id' => 'hinode_search',
|
||||
'plugin' => 'node_search',
|
||||
'label' => 'Search of magnetic activity of the Sun',
|
||||
'path' => 'sun',
|
||||
]);
|
||||
$search_page->save();
|
||||
return $search_page;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/search_page/search_page/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'search_page--search_page',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'configuration' => [
|
||||
'rankings' => [],
|
||||
],
|
||||
'dependencies' => [
|
||||
'module' => [
|
||||
'node',
|
||||
],
|
||||
],
|
||||
'label' => 'Search of magnetic activity of the Sun',
|
||||
'langcode' => 'en',
|
||||
'path' => 'sun',
|
||||
'plugin' => 'node_search',
|
||||
'status' => TRUE,
|
||||
'weight' => 0,
|
||||
'drupal_internal__id' => 'hinode_search',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
// @todo Update in https://www.drupal.org/node/2300677.
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessMessage($method) {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
return "The 'access content' permission is required.";
|
||||
|
||||
default:
|
||||
return parent::getExpectedUnauthorizedAccessMessage($method);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessCacheability() {
|
||||
// @see \Drupal\search\SearchPageAccessControlHandler::checkAccess()
|
||||
return parent::getExpectedUnauthorizedAccessCacheability()
|
||||
->addCacheTags(['config:search.page.hinode_search']);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* @covers \Drupal\jsonapi\Form\JsonApiSettingsForm
|
||||
* @group jsonapi
|
||||
*/
|
||||
class SettingsFormTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['jsonapi'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* Tests the JSON:API settings form.
|
||||
*/
|
||||
public function testSettingsForm(): void {
|
||||
$account = $this->drupalCreateUser(['administer site configuration']);
|
||||
$this->drupalLogin($account);
|
||||
$this->drupalGet('/admin/config/services/jsonapi');
|
||||
|
||||
$page = $this->getSession()->getPage();
|
||||
$page->selectFieldOption('read_only', 'rw');
|
||||
$page->pressButton('Save configuration');
|
||||
$assert_session = $this->assertSession();
|
||||
$assert_session->pageTextContains('The configuration options have been saved.');
|
||||
$assert_session->fieldValueEquals('read_only', 'rw');
|
||||
|
||||
$page->selectFieldOption('read_only', 'r');
|
||||
$page->pressButton('Save configuration');
|
||||
$assert_session->fieldValueEquals('read_only', 'r');
|
||||
$assert_session->pageTextContains('The configuration options have been saved.');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\shortcut\Entity\ShortcutSet;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "ShortcutSet" config entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class ShortcutSetTest extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['shortcut'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'shortcut_set';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'shortcut_set--shortcut_set';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\shortcut\ShortcutSetInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
$this->grantPermissionsToTestedRole(['access shortcuts']);
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
case 'PATCH':
|
||||
$this->grantPermissionsToTestedRole(['access shortcuts', 'customize shortcut links']);
|
||||
break;
|
||||
|
||||
case 'DELETE':
|
||||
$this->grantPermissionsToTestedRole(['administer shortcuts']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessMessage($method) {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
return "The 'access shortcuts' permission is required.";
|
||||
|
||||
default:
|
||||
return parent::getExpectedUnauthorizedAccessMessage($method);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
$set = ShortcutSet::create([
|
||||
'id' => 'llama-set',
|
||||
'label' => 'Llama Set',
|
||||
]);
|
||||
$set->save();
|
||||
return $set;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/shortcut_set/shortcut_set/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'shortcut_set--shortcut_set',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'label' => 'Llama Set',
|
||||
'status' => TRUE,
|
||||
'langcode' => 'en',
|
||||
'dependencies' => [],
|
||||
'drupal_internal__id' => 'llama-set',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
// @todo Update in https://www.drupal.org/node/2300677.
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
209
web/core/modules/jsonapi/tests/src/Functional/ShortcutTest.php
Normal file
209
web/core/modules/jsonapi/tests/src/Functional/ShortcutTest.php
Normal file
@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\shortcut\Entity\Shortcut;
|
||||
use Drupal\shortcut\Entity\ShortcutSet;
|
||||
use Drupal\Tests\jsonapi\Traits\CommonCollectionFilterAccessTestPatternsTrait;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "Shortcut" content entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class ShortcutTest extends ResourceTestBase {
|
||||
|
||||
use CommonCollectionFilterAccessTestPatternsTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['comment', 'shortcut'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'shortcut';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'shortcut--default';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\shortcut\ShortcutInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $patchProtectedFieldNames = [];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['access shortcuts', 'customize shortcut links']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
$shortcut = Shortcut::create([
|
||||
'shortcut_set' => 'default',
|
||||
'title' => 'Comments',
|
||||
'weight' => -20,
|
||||
'link' => [
|
||||
'uri' => 'internal:/user/logout',
|
||||
],
|
||||
]);
|
||||
$shortcut->save();
|
||||
|
||||
return $shortcut;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/shortcut/default/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'shortcut--default',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'title' => 'Comments',
|
||||
'link' => [
|
||||
'uri' => 'internal:/user/logout',
|
||||
'title' => NULL,
|
||||
'options' => [],
|
||||
],
|
||||
'langcode' => 'en',
|
||||
'default_langcode' => TRUE,
|
||||
'weight' => -20,
|
||||
'drupal_internal__id' => (int) $this->entity->id(),
|
||||
],
|
||||
'relationships' => [
|
||||
'shortcut_set' => [
|
||||
'data' => [
|
||||
'type' => 'shortcut_set--shortcut_set',
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => 'default',
|
||||
],
|
||||
'id' => ShortcutSet::load('default')->uuid(),
|
||||
],
|
||||
'links' => [
|
||||
'related' => ['href' => $self_url . '/shortcut_set'],
|
||||
'self' => ['href' => $self_url . '/relationships/shortcut_set'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
return [
|
||||
'data' => [
|
||||
'type' => 'shortcut--default',
|
||||
'attributes' => [
|
||||
'title' => 'Comments',
|
||||
'link' => [
|
||||
'uri' => 'internal:/',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessMessage($method): string {
|
||||
return "The shortcut set must be the currently displayed set for the user and the user must have 'access shortcuts' AND 'customize shortcut links' permissions.";
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function testCollectionFilterAccess(): void {
|
||||
$label_field_name = 'title';
|
||||
// Verify the expected behavior in the common case: default shortcut set.
|
||||
$this->grantPermissionsToTestedRole(['customize shortcut links']);
|
||||
$this->doTestCollectionFilterAccessBasedOnPermissions($label_field_name, 'access shortcuts');
|
||||
|
||||
$alternate_shortcut_set = ShortcutSet::create([
|
||||
'id' => 'alternate',
|
||||
'label' => 'Alternate',
|
||||
]);
|
||||
$alternate_shortcut_set->save();
|
||||
$this->entity->shortcut_set = $alternate_shortcut_set->id();
|
||||
$this->entity->save();
|
||||
|
||||
$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());
|
||||
|
||||
// No results because the current user does not have access to shortcuts
|
||||
// not in the user's assigned set or the default set.
|
||||
$response = $this->request('GET', $collection_filter_url, $request_options);
|
||||
$doc = $this->getDocumentFromResponse($response);
|
||||
$this->assertCount(0, $doc['data']);
|
||||
|
||||
// Assign the alternate shortcut set to the current user.
|
||||
$this->container->get('entity_type.manager')->getStorage('shortcut_set')->assignUser($alternate_shortcut_set, $this->account);
|
||||
|
||||
// 1 result because the alternate shortcut set is now assigned to the
|
||||
// current user.
|
||||
$response = $this->request('GET', $collection_filter_url, $request_options);
|
||||
$doc = $this->getDocumentFromResponse($response);
|
||||
$this->assertCount(1, $doc['data']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static function getExpectedCollectionCacheability(AccountInterface $account, array $collection, ?array $sparse_fieldset = NULL, $filtered = FALSE) {
|
||||
$cacheability = parent::getExpectedCollectionCacheability($account, $collection, $sparse_fieldset, $filtered);
|
||||
if ($filtered) {
|
||||
$cacheability->addCacheContexts(['user']);
|
||||
}
|
||||
return $cacheability;
|
||||
}
|
||||
|
||||
}
|
||||
513
web/core/modules/jsonapi/tests/src/Functional/TermTest.php
Normal file
513
web/core/modules/jsonapi/tests/src/Functional/TermTest.php
Normal file
@ -0,0 +1,513 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\taxonomy\Entity\Term;
|
||||
use Drupal\taxonomy\Entity\Vocabulary;
|
||||
use Drupal\Tests\jsonapi\Traits\CommonCollectionFilterAccessTestPatternsTrait;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "Term" content entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class TermTest extends ResourceTestBase {
|
||||
|
||||
use CommonCollectionFilterAccessTestPatternsTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['taxonomy', 'path'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'taxonomy_term';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'taxonomy_term--camelids';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeIsVersionable = TRUE;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $patchProtectedFieldNames = [
|
||||
'changed' => NULL,
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\taxonomy\TermInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
$this->grantPermissionsToTestedRole(['access content', 'view vocabulary labels']);
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
$this->grantPermissionsToTestedRole(['create terms in camelids']);
|
||||
break;
|
||||
|
||||
case 'PATCH':
|
||||
// Grant the 'create url aliases' permission to test the case when
|
||||
// the path field is accessible, see
|
||||
// \Drupal\Tests\rest\Functional\EntityResource\Node\NodeResourceTestBase
|
||||
// for a negative test.
|
||||
$this->grantPermissionsToTestedRole(['edit terms in camelids', 'create url aliases']);
|
||||
break;
|
||||
|
||||
case 'DELETE':
|
||||
$this->grantPermissionsToTestedRole(['delete terms in camelids']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpRevisionAuthorization($method): void {
|
||||
parent::setUpRevisionAuthorization($method);
|
||||
$this->grantPermissionsToTestedRole(['administer taxonomy']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
$vocabulary = Vocabulary::load('camelids');
|
||||
if (!$vocabulary) {
|
||||
// Create a "Camelids" vocabulary.
|
||||
$vocabulary = Vocabulary::create([
|
||||
'name' => 'Camelids',
|
||||
'vid' => 'camelids',
|
||||
]);
|
||||
$vocabulary->save();
|
||||
}
|
||||
|
||||
// Create a "Llama" taxonomy term.
|
||||
$term = Term::create(['vid' => $vocabulary->id()])
|
||||
->setName('Llama')
|
||||
->setDescription("It is a little known fact that llamas cannot count higher than seven.")
|
||||
->setChangedTime(123456789)
|
||||
->set('path', '/llama');
|
||||
$term->save();
|
||||
|
||||
return $term;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$base_url = Url::fromUri('base:/jsonapi/taxonomy_term/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);
|
||||
|
||||
// We test with multiple parent terms, and combinations thereof.
|
||||
// @see ::createEntity()
|
||||
// @see ::testGetIndividual()
|
||||
// @see ::testGetIndividualTermWithParent()
|
||||
// @see ::providerTestGetIndividualTermWithParent()
|
||||
$parent_term_ids = [];
|
||||
for ($i = 0; $i < $this->entity->get('parent')->count(); $i++) {
|
||||
$parent_term_ids[$i] = (int) $this->entity->get('parent')[$i]->target_id;
|
||||
}
|
||||
|
||||
$expected_parent_normalization = FALSE;
|
||||
switch ($parent_term_ids) {
|
||||
case [0]:
|
||||
$expected_parent_normalization = [
|
||||
'data' => [
|
||||
[
|
||||
'id' => 'virtual',
|
||||
'type' => 'taxonomy_term--camelids',
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'help' => [
|
||||
'href' => 'https://www.drupal.org/docs/8/modules/json-api/core-concepts#virtual',
|
||||
'meta' => [
|
||||
'about' => "Usage and meaning of the 'virtual' resource identifier.",
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'links' => [
|
||||
'related' => ['href' => $base_url->toString() . '/parent' . $version_query_string],
|
||||
'self' => ['href' => $base_url->toString() . '/relationships/parent' . $version_query_string],
|
||||
],
|
||||
];
|
||||
break;
|
||||
|
||||
case [2]:
|
||||
$expected_parent_normalization = [
|
||||
'data' => [
|
||||
[
|
||||
'id' => Term::load(2)->uuid(),
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => 2,
|
||||
],
|
||||
'type' => 'taxonomy_term--camelids',
|
||||
],
|
||||
],
|
||||
'links' => [
|
||||
'related' => ['href' => $base_url->toString() . '/parent' . $version_query_string],
|
||||
'self' => ['href' => $base_url->toString() . '/relationships/parent' . $version_query_string],
|
||||
],
|
||||
];
|
||||
break;
|
||||
|
||||
case [0, 2]:
|
||||
$expected_parent_normalization = [
|
||||
'data' => [
|
||||
[
|
||||
'id' => 'virtual',
|
||||
'type' => 'taxonomy_term--camelids',
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'help' => [
|
||||
'href' => 'https://www.drupal.org/docs/8/modules/json-api/core-concepts#virtual',
|
||||
'meta' => [
|
||||
'about' => "Usage and meaning of the 'virtual' resource identifier.",
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'id' => Term::load(2)->uuid(),
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => 2,
|
||||
],
|
||||
'type' => 'taxonomy_term--camelids',
|
||||
],
|
||||
],
|
||||
'links' => [
|
||||
'related' => ['href' => $base_url->toString() . '/parent' . $version_query_string],
|
||||
'self' => ['href' => $base_url->toString() . '/relationships/parent' . $version_query_string],
|
||||
],
|
||||
];
|
||||
break;
|
||||
|
||||
case [3, 2]:
|
||||
$expected_parent_normalization = [
|
||||
'data' => [
|
||||
[
|
||||
'id' => Term::load(3)->uuid(),
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => 3,
|
||||
],
|
||||
'type' => 'taxonomy_term--camelids',
|
||||
],
|
||||
[
|
||||
'id' => Term::load(2)->uuid(),
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => 2,
|
||||
],
|
||||
'type' => 'taxonomy_term--camelids',
|
||||
],
|
||||
],
|
||||
'links' => [
|
||||
'related' => ['href' => $base_url->toString() . '/parent' . $version_query_string],
|
||||
'self' => ['href' => $base_url->toString() . '/relationships/parent' . $version_query_string],
|
||||
],
|
||||
];
|
||||
break;
|
||||
}
|
||||
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $base_url->toString()],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'taxonomy_term--camelids',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url->toString()],
|
||||
],
|
||||
'attributes' => [
|
||||
'changed' => (new \DateTime())->setTimestamp($this->entity->getChangedTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
|
||||
'default_langcode' => TRUE,
|
||||
'description' => [
|
||||
'value' => 'It is a little known fact that llamas cannot count higher than seven.',
|
||||
'format' => NULL,
|
||||
'processed' => "<p>It is a little known fact that llamas cannot count higher than seven.</p>\n",
|
||||
],
|
||||
'langcode' => 'en',
|
||||
'name' => 'Llama',
|
||||
'path' => [
|
||||
'alias' => '/llama',
|
||||
'pid' => 1,
|
||||
'langcode' => 'en',
|
||||
],
|
||||
'weight' => 0,
|
||||
'drupal_internal__tid' => 1,
|
||||
'status' => TRUE,
|
||||
'drupal_internal__revision_id' => 1,
|
||||
'revision_created' => (new \DateTime())->setTimestamp((int) $this->entity->getRevisionCreationTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
|
||||
// @todo Attempt to remove this in https://www.drupal.org/project/drupal/issues/2933518.
|
||||
'revision_translation_affected' => TRUE,
|
||||
],
|
||||
'relationships' => [
|
||||
'parent' => $expected_parent_normalization,
|
||||
'vid' => [
|
||||
'data' => [
|
||||
'id' => Vocabulary::load('camelids')->uuid(),
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => 'camelids',
|
||||
],
|
||||
'type' => 'taxonomy_vocabulary--taxonomy_vocabulary',
|
||||
],
|
||||
'links' => [
|
||||
'related' => ['href' => $base_url->toString() . '/vid' . $version_query_string],
|
||||
'self' => ['href' => $base_url->toString() . '/relationships/vid' . $version_query_string],
|
||||
],
|
||||
],
|
||||
'revision_user' => [
|
||||
'data' => NULL,
|
||||
'links' => [
|
||||
'related' => [
|
||||
'href' => $base_url->toString() . '/revision_user' . $version_query_string,
|
||||
],
|
||||
'self' => [
|
||||
'href' => $base_url->toString() . '/relationships/revision_user' . $version_query_string,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedGetRelationshipDocumentData($relationship_field_name, ?EntityInterface $entity = NULL) {
|
||||
$data = parent::getExpectedGetRelationshipDocumentData($relationship_field_name, $entity);
|
||||
if ($relationship_field_name === 'parent') {
|
||||
$data = [
|
||||
0 => [
|
||||
'id' => 'virtual',
|
||||
'type' => 'taxonomy_term--camelids',
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'help' => [
|
||||
'href' => 'https://www.drupal.org/docs/8/modules/json-api/core-concepts#virtual',
|
||||
'meta' => [
|
||||
'about' => "Usage and meaning of the 'virtual' resource identifier.",
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
return [
|
||||
'data' => [
|
||||
'type' => 'taxonomy_term--camelids',
|
||||
'attributes' => [
|
||||
'name' => 'Drama llama',
|
||||
'description' => [
|
||||
'value' => 'Drama llamas are the coolest camelids.',
|
||||
'format' => NULL,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessMessage($method) {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
return "The 'access content' permission is required and the taxonomy term must be published.";
|
||||
|
||||
case 'POST':
|
||||
return "The following permissions are required: 'create terms in camelids' OR 'administer taxonomy'.";
|
||||
|
||||
case 'PATCH':
|
||||
return "The following permissions are required: 'edit terms in camelids' OR 'administer taxonomy'.";
|
||||
|
||||
case 'DELETE':
|
||||
return "The following permissions are required: 'delete terms in camelids' OR 'administer taxonomy'.";
|
||||
|
||||
default:
|
||||
return parent::getExpectedUnauthorizedAccessMessage($method);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessCacheability() {
|
||||
$cacheability = parent::getExpectedUnauthorizedAccessCacheability();
|
||||
$cacheability->addCacheableDependency($this->entity);
|
||||
return $cacheability;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests PATCHing a term's path.
|
||||
*
|
||||
* For a negative test, see the similar test coverage for Node.
|
||||
*
|
||||
* @see \Drupal\Tests\jsonapi\Functional\NodeTest::testPatchPath()
|
||||
* @see \Drupal\Tests\rest\Functional\EntityResource\Node\NodeResourceTestBase::testPatchPath()
|
||||
*/
|
||||
public function testPatchPath(): void {
|
||||
$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/drupal/issues/2878463.
|
||||
$url = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), ['entity' => $this->entity->uuid()]);
|
||||
// $url = $this->entity->toUrl('jsonapi');
|
||||
$request_options = [];
|
||||
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
|
||||
$request_options[RequestOptions::HEADERS]['Content-Type'] = 'application/vnd.api+json';
|
||||
$request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions());
|
||||
|
||||
// GET term's current normalization.
|
||||
$response = $this->request('GET', $url, $request_options);
|
||||
$normalization = $this->getDocumentFromResponse($response);
|
||||
|
||||
// Change term's path alias.
|
||||
$normalization['data']['attributes']['path']['alias'] .= 's-rule-the-world';
|
||||
|
||||
// Create term PATCH request.
|
||||
$request_options[RequestOptions::BODY] = Json::encode($normalization);
|
||||
|
||||
// PATCH request: 200.
|
||||
$response = $this->request('PATCH', $url, $request_options);
|
||||
$updated_normalization = $this->getDocumentFromResponse($response);
|
||||
$this->assertResourceResponse(200, FALSE, $response);
|
||||
$this->assertSame($normalization['data']['attributes']['path']['alias'], $updated_normalization['data']['attributes']['path']['alias']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedCacheTags(?array $sparse_fieldset = NULL) {
|
||||
$tags = parent::getExpectedCacheTags($sparse_fieldset);
|
||||
if ($sparse_fieldset === NULL || in_array('description', $sparse_fieldset)) {
|
||||
$tags = Cache::mergeTags($tags, ['config:filter.format.plain_text', 'config:filter.settings']);
|
||||
}
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedCacheContexts(?array $sparse_fieldset = NULL) {
|
||||
$contexts = parent::getExpectedCacheContexts($sparse_fieldset);
|
||||
if ($sparse_fieldset === NULL || in_array('description', $sparse_fieldset)) {
|
||||
$contexts = Cache::mergeContexts($contexts, ['languages:language_interface', 'theme']);
|
||||
}
|
||||
return $contexts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests GETting a term with a parent term other than the default <root> (0).
|
||||
*
|
||||
* @see ::getExpectedNormalizedEntity()
|
||||
*
|
||||
* @dataProvider providerTestGetIndividualTermWithParent
|
||||
*/
|
||||
public function testGetIndividualTermWithParent(array $parent_term_ids): void {
|
||||
// Create all possible parent terms.
|
||||
Term::create(['vid' => Vocabulary::load('camelids')->id()])
|
||||
->setName('Lamoids')
|
||||
->save();
|
||||
Term::create(['vid' => Vocabulary::load('camelids')->id()])
|
||||
->setName('Camels')
|
||||
->save();
|
||||
|
||||
// Modify the entity under test to use the provided parent terms.
|
||||
$this->entity->set('parent', $parent_term_ids)->save();
|
||||
|
||||
// @todo Remove line below in favor of commented line in https://www.drupal.org/project/drupal/issues/2878463.
|
||||
$url = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), ['entity' => $this->entity->uuid()]);
|
||||
// $url = $this->entity->toUrl('jsonapi');
|
||||
$request_options = [];
|
||||
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
|
||||
$request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions());
|
||||
$this->setUpAuthorization('GET');
|
||||
$response = $this->request('GET', $url, $request_options);
|
||||
$this->assertSameDocument($this->getExpectedDocument(), Json::decode($response->getBody()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for ::testGetIndividualTermWithParent().
|
||||
*/
|
||||
public static function providerTestGetIndividualTermWithParent() {
|
||||
return [
|
||||
'root parent: [0] (= no parent)' => [
|
||||
[0],
|
||||
],
|
||||
'non-root parent: [2]' => [
|
||||
[2],
|
||||
],
|
||||
'multiple parents: [0,2] (root + non-root parent)' => [
|
||||
[0, 2],
|
||||
],
|
||||
'multiple parents: [3,2] (both non-root parents)' => [
|
||||
[3, 2],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function testCollectionFilterAccess(): void {
|
||||
$this->doTestCollectionFilterAccessBasedOnPermissions('name', 'access content');
|
||||
}
|
||||
|
||||
}
|
||||
841
web/core/modules/jsonapi/tests/src/Functional/UserTest.php
Normal file
841
web/core/modules/jsonapi/tests/src/Functional/UserTest.php
Normal file
@ -0,0 +1,841 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\user\Entity\User;
|
||||
use Drupal\user\UserInterface;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "User" content entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class UserTest extends ResourceTestBase {
|
||||
|
||||
const BATCH_TEST_NODE_COUNT = 15;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['user', 'jsonapi_test_user', 'node'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'user';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'user--user';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $patchProtectedFieldNames = [
|
||||
'changed' => NULL,
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $anonymousUsersCanViewLabels = TRUE;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\taxonomy\TermInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $labelFieldName = 'display_name';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $firstCreatedEntityId = 4;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $secondCreatedEntityId = 5;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
// @todo Remove this in
|
||||
$this->grantPermissionsToTestedRole(['access content']);
|
||||
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
$this->grantPermissionsToTestedRole(['access user profiles']);
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
case 'PATCH':
|
||||
case 'DELETE':
|
||||
$this->grantPermissionsToTestedRole(['administer users']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
// Create a "Llama" user.
|
||||
$user = User::create(['created' => 123456789]);
|
||||
$user->setUsername('Llama')
|
||||
->setChangedTime(123456789)
|
||||
->activate()
|
||||
->save();
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createAnotherEntity($key) {
|
||||
/** @var \Drupal\user\UserInterface $user */
|
||||
$user = $this->getEntityDuplicate($this->entity, $key);
|
||||
$user->setUsername($user->label() . '_' . $key);
|
||||
$user->setEmail("$key@example.com");
|
||||
$user->save();
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function doTestDeleteIndividual(): void {
|
||||
$this->config('user.settings')->set('cancel_method', 'user_cancel_delete')->save(TRUE);
|
||||
|
||||
parent::doTestDeleteIndividual();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/user/user/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'user--user',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'display_name' => 'Llama',
|
||||
'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',
|
||||
'name' => 'Llama',
|
||||
'drupal_internal__uid' => 3,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedCacheContexts(?array $sparse_fieldset = NULL) {
|
||||
$cache_contexts = parent::getExpectedCacheContexts($sparse_fieldset);
|
||||
if ($sparse_fieldset === NULL || !empty(array_intersect(['mail', 'display_name'], $sparse_fieldset))) {
|
||||
$cache_contexts = Cache::mergeContexts($cache_contexts, ['user']);
|
||||
}
|
||||
return $cache_contexts;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
return [
|
||||
'data' => [
|
||||
'type' => 'user--user',
|
||||
'attributes' => [
|
||||
'name' => 'Drama llama',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPatchDocument() {
|
||||
return [
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'user--user',
|
||||
'attributes' => [
|
||||
'name' => 'Drama llama 2',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessMessage($method) {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
return "The 'access user profiles' permission is required and the user must be active.";
|
||||
|
||||
case 'PATCH':
|
||||
return "Users can only update their own account, unless they have the 'administer users' permission.";
|
||||
|
||||
case 'DELETE':
|
||||
return "The 'cancel account' permission is required.";
|
||||
|
||||
default:
|
||||
return parent::getExpectedUnauthorizedAccessMessage($method);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests PATCHing security-sensitive base fields of the logged in account.
|
||||
*/
|
||||
public function testPatchDxForSecuritySensitiveBaseFields(): void {
|
||||
// @todo Remove line below in favor of commented line in https://www.drupal.org/project/drupal/issues/2878463.
|
||||
$url = Url::fromRoute(sprintf('jsonapi.user--user.individual'), ['entity' => $this->account->uuid()]);
|
||||
/* $url = $this->account->toUrl('jsonapi'); */
|
||||
|
||||
// Since this test must be performed by the user that is being modified,
|
||||
// we must use $this->account, not $this->entity.
|
||||
$request_options = [];
|
||||
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
|
||||
$request_options[RequestOptions::HEADERS]['Content-Type'] = 'application/vnd.api+json';
|
||||
$request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions());
|
||||
|
||||
$response = $this->request('GET', $url, $request_options);
|
||||
$original_normalization = $this->getDocumentFromResponse($response);
|
||||
|
||||
// Test case 1: changing email.
|
||||
$normalization = $original_normalization;
|
||||
$normalization['data']['attributes']['mail'] = 'new-email@example.com';
|
||||
$request_options[RequestOptions::BODY] = Json::encode($normalization);
|
||||
|
||||
// DX: 405 when read-only mode is enabled.
|
||||
$response = $this->request('PATCH', $url, $request_options);
|
||||
$this->assertResourceErrorResponse(405, sprintf("JSON:API is configured to accept only read operations. Site administrators can configure this at %s.", Url::fromUri('base:/admin/config/services/jsonapi')->setAbsolute()->toString(TRUE)->getGeneratedUrl()), $url, $response);
|
||||
$this->assertSame(['GET'], $response->getHeader('Allow'));
|
||||
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
|
||||
// DX: 422 when changing email without providing the password.
|
||||
$response = $this->request('PATCH', $url, $request_options);
|
||||
$this->assertResourceErrorResponse(422, 'mail: Your current password is missing or incorrect; it\'s required to change the Email.', NULL, $response, '/data/attributes/mail');
|
||||
|
||||
$normalization['data']['attributes']['pass']['existing'] = 'wrong';
|
||||
$request_options[RequestOptions::BODY] = Json::encode($normalization);
|
||||
|
||||
// DX: 422 when changing email while providing a wrong password.
|
||||
$response = $this->request('PATCH', $url, $request_options);
|
||||
$this->assertResourceErrorResponse(422, 'mail: Your current password is missing or incorrect; it\'s required to change the Email.', NULL, $response, '/data/attributes/mail');
|
||||
|
||||
$normalization['data']['attributes']['pass']['existing'] = $this->account->passRaw;
|
||||
$request_options[RequestOptions::BODY] = Json::encode($normalization);
|
||||
|
||||
// 200 for well-formed request.
|
||||
$response = $this->request('PATCH', $url, $request_options);
|
||||
$this->assertResourceResponse(200, FALSE, $response);
|
||||
|
||||
// Test case 2: changing password.
|
||||
$normalization = $this->getDocumentFromResponse($response);
|
||||
$normalization['data']['attributes']['mail'] = 'new-email@example.com';
|
||||
$new_password = $this->randomString();
|
||||
$normalization['data']['attributes']['pass']['value'] = $new_password;
|
||||
$request_options[RequestOptions::BODY] = Json::encode($normalization);
|
||||
|
||||
// DX: 422 when changing password without providing the current password.
|
||||
$response = $this->request('PATCH', $url, $request_options);
|
||||
$this->assertResourceErrorResponse(422, 'pass: Your current password is missing or incorrect; it\'s required to change the Password.', NULL, $response, '/data/attributes/pass');
|
||||
|
||||
$normalization['data']['attributes']['pass']['existing'] = $this->account->passRaw;
|
||||
$request_options[RequestOptions::BODY] = Json::encode($normalization);
|
||||
|
||||
// 200 for well-formed request.
|
||||
$response = $this->request('PATCH', $url, $request_options);
|
||||
$this->assertResourceResponse(200, FALSE, $response);
|
||||
|
||||
// Verify that we can log in with the new password.
|
||||
$this->assertRpcLogin($this->account->getAccountName(), $new_password);
|
||||
|
||||
// Update password in $this->account, prepare for future requests.
|
||||
$this->account->passRaw = $new_password;
|
||||
$request_options = [];
|
||||
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
|
||||
$request_options[RequestOptions::HEADERS]['Content-Type'] = 'application/vnd.api+json';
|
||||
$request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions());
|
||||
|
||||
// Test case 3: changing name.
|
||||
$normalization = $this->getDocumentFromResponse($response);
|
||||
$normalization['data']['attributes']['mail'] = 'new-email@example.com';
|
||||
$normalization['data']['attributes']['pass']['existing'] = $new_password;
|
||||
$normalization['data']['attributes']['name'] = 'Cooler Llama';
|
||||
$request_options[RequestOptions::BODY] = Json::encode($normalization);
|
||||
|
||||
// DX: 403 when modifying username without required permission.
|
||||
$response = $this->request('PATCH', $url, $request_options);
|
||||
$this->assertResourceErrorResponse(403, 'The current user is not allowed to PATCH the selected field (name).', $url, $response, '/data/attributes/name');
|
||||
|
||||
$this->grantPermissionsToTestedRole(['change own username']);
|
||||
|
||||
// 200 for well-formed request.
|
||||
$response = $this->request('PATCH', $url, $request_options);
|
||||
$this->assertResourceResponse(200, FALSE, $response);
|
||||
|
||||
// Verify that we can log in with the new username.
|
||||
$this->assertRpcLogin('Cooler Llama', $new_password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that logging in with the given username and password works.
|
||||
*
|
||||
* @param string $username
|
||||
* The username to log in with.
|
||||
* @param string $password
|
||||
* The password to log in with.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function assertRpcLogin(string $username, string $password): void {
|
||||
$request_body = [
|
||||
'name' => $username,
|
||||
'pass' => $password,
|
||||
];
|
||||
$request_options = [
|
||||
RequestOptions::HEADERS => [],
|
||||
RequestOptions::BODY => Json::encode($request_body),
|
||||
];
|
||||
$response = $this->request('POST', Url::fromRoute('user.login.http')->setRouteParameter('_format', 'json'), $request_options);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests PATCHing security-sensitive base fields to change other users.
|
||||
*/
|
||||
public function testPatchSecurityOtherUser(): void {
|
||||
// @todo Remove line below in favor of commented line in https://www.drupal.org/project/drupal/issues/2878463.
|
||||
$url = Url::fromRoute(sprintf('jsonapi.user--user.individual'), ['entity' => $this->account->uuid()]);
|
||||
/* $url = $this->account->toUrl('jsonapi'); */
|
||||
|
||||
$original_normalization = $this->normalize($this->account, $url);
|
||||
|
||||
// Since this test must be performed by the user that is being modified,
|
||||
// we must use $this->account, not $this->entity.
|
||||
$request_options = [];
|
||||
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
|
||||
$request_options[RequestOptions::HEADERS]['Content-Type'] = 'application/vnd.api+json';
|
||||
$request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions());
|
||||
|
||||
$normalization = $original_normalization;
|
||||
$normalization['data']['attributes']['mail'] = 'new-email@example.com';
|
||||
$request_options[RequestOptions::BODY] = Json::encode($normalization);
|
||||
|
||||
// DX: 405 when read-only mode is enabled.
|
||||
$response = $this->request('PATCH', $url, $request_options);
|
||||
$this->assertResourceErrorResponse(405, sprintf("JSON:API is configured to accept only read operations. Site administrators can configure this at %s.", Url::fromUri('base:/admin/config/services/jsonapi')->setAbsolute()->toString(TRUE)->getGeneratedUrl()), $url, $response);
|
||||
$this->assertSame(['GET'], $response->getHeader('Allow'));
|
||||
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
|
||||
// Try changing user 1's email.
|
||||
$user1 = $original_normalization;
|
||||
$user1['data']['attributes']['mail'] = 'another_email_address@example.com';
|
||||
$user1['data']['attributes']['uid'] = 1;
|
||||
$user1['data']['attributes']['name'] = 'another_user_name';
|
||||
$user1['data']['attributes']['pass']['existing'] = $this->account->passRaw;
|
||||
$request_options[RequestOptions::BODY] = Json::encode($user1);
|
||||
$response = $this->request('PATCH', $url, $request_options);
|
||||
// Ensure the email address has not changed.
|
||||
$this->assertEquals('admin@example.com', $this->entityStorage->loadUnchanged(1)->getEmail());
|
||||
$this->assertResourceErrorResponse(403, 'The current user is not allowed to PATCH the selected field (uid). The entity ID cannot be changed.', $url, $response, '/data/attributes/uid');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests GETting privacy-sensitive base fields.
|
||||
*/
|
||||
public function testGetMailFieldOnlyVisibleToOwner(): void {
|
||||
// Create user B, with the same roles (and hence permissions) as user A.
|
||||
$user_a = $this->account;
|
||||
$pass = \Drupal::service('password_generator')->generate();
|
||||
$user_b = User::create([
|
||||
'name' => 'sibling-of-' . $user_a->getAccountName(),
|
||||
'mail' => 'sibling-of-' . $user_a->getAccountName() . '@example.com',
|
||||
'pass' => $pass,
|
||||
'status' => 1,
|
||||
'roles' => $user_a->getRoles(),
|
||||
]);
|
||||
$user_b->save();
|
||||
$user_b->passRaw = $pass;
|
||||
|
||||
// Grant permission to role that both users use.
|
||||
$this->grantPermissionsToTestedRole(['access user profiles']);
|
||||
|
||||
$collection_url = Url::fromRoute('jsonapi.user--user.collection', [], ['query' => ['sort' => 'drupal_internal__uid']]);
|
||||
// @todo Remove line below in favor of commented line in https://www.drupal.org/project/drupal/issues/2878463.
|
||||
$user_a_url = Url::fromRoute(sprintf('jsonapi.user--user.individual'), ['entity' => $user_a->uuid()]);
|
||||
/* $user_a_url = $user_a->toUrl('jsonapi'); */
|
||||
$request_options = [];
|
||||
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
|
||||
$request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions());
|
||||
|
||||
// Viewing user A as user A: "mail" field is accessible.
|
||||
$response = $this->request('GET', $user_a_url, $request_options);
|
||||
$doc = $this->getDocumentFromResponse($response);
|
||||
$this->assertArrayHasKey('mail', $doc['data']['attributes']);
|
||||
// Also when looking at the collection.
|
||||
$response = $this->request('GET', $collection_url, $request_options);
|
||||
$doc = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame($user_a->uuid(), $doc['data']['2']['id']);
|
||||
$this->assertArrayHasKey('mail', $doc['data'][2]['attributes'], "Own user--user resource's 'mail' field is visible.");
|
||||
$this->assertSame($user_b->uuid(), $doc['data'][count($doc['data']) - 1]['id']);
|
||||
$this->assertArrayNotHasKey('mail', $doc['data'][count($doc['data']) - 1]['attributes']);
|
||||
|
||||
// Now request the same URLs, but as user B (same roles/permissions).
|
||||
$this->account = $user_b;
|
||||
$request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions());
|
||||
// Viewing user A as user B: "mail" field should be inaccessible.
|
||||
$response = $this->request('GET', $user_a_url, $request_options);
|
||||
$doc = $this->getDocumentFromResponse($response);
|
||||
$this->assertArrayNotHasKey('mail', $doc['data']['attributes']);
|
||||
// Also when looking at the collection.
|
||||
$response = $this->request('GET', $collection_url, $request_options);
|
||||
$doc = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame($user_a->uuid(), $doc['data']['2']['id']);
|
||||
$this->assertArrayNotHasKey('mail', $doc['data'][2]['attributes']);
|
||||
$this->assertSame($user_b->uuid(), $doc['data'][count($doc['data']) - 1]['id']);
|
||||
$this->assertArrayHasKey('mail', $doc['data'][count($doc['data']) - 1]['attributes']);
|
||||
|
||||
// Now grant permission to view user email addresses and verify.
|
||||
$this->grantPermissionsToTestedRole(['view user email addresses']);
|
||||
// Viewing user A as user B: "mail" field should be accessible.
|
||||
$response = $this->request('GET', $user_a_url, $request_options);
|
||||
$doc = $this->getDocumentFromResponse($response);
|
||||
$this->assertArrayHasKey('mail', $doc['data']['attributes']);
|
||||
// Also when looking at the collection.
|
||||
$response = $this->request('GET', $collection_url, $request_options);
|
||||
$doc = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame($user_a->uuid(), $doc['data']['2']['id']);
|
||||
$this->assertArrayHasKey('mail', $doc['data'][2]['attributes']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests good error DX when trying to filter users by role.
|
||||
*/
|
||||
public function testQueryInvolvingRoles(): void {
|
||||
$this->setUpAuthorization('GET');
|
||||
|
||||
$collection_url = Url::fromRoute('jsonapi.user--user.collection', [], ['query' => ['filter[roles.id][value]' => 'e9b1de3f-9517-4c27-bef0-0301229de792']]);
|
||||
$request_options = [];
|
||||
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
|
||||
$request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions());
|
||||
|
||||
// The 'administer users' permission is required to filter by role entities.
|
||||
$this->grantPermissionsToTestedRole(['administer users']);
|
||||
|
||||
$response = $this->request('GET', $collection_url, $request_options);
|
||||
$expected_cache_contexts = ['url.path', 'url.query_args', 'url.site'];
|
||||
$this->assertResourceErrorResponse(400, "Filtering on config entities is not supported by Drupal's entity API. You tried to filter on a Role config entity.", $collection_url, $response, FALSE, ['4xx-response', 'http_response'], $expected_cache_contexts, NULL, 'MISS');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the collection contains the anonymous user.
|
||||
*/
|
||||
public function testCollectionContainsAnonymousUser(): void {
|
||||
$url = Url::fromRoute('jsonapi.user--user.collection', [], ['query' => ['sort' => 'drupal_internal__uid']]);
|
||||
$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);
|
||||
$doc = $this->getDocumentFromResponse($response);
|
||||
|
||||
$this->assertCount(4, $doc['data']);
|
||||
$this->assertSame(User::load(0)->uuid(), $doc['data'][0]['id']);
|
||||
$this->assertSame('User 0', $doc['data'][0]['attributes']['display_name']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function testCollectionFilterAccess(): void {
|
||||
// Set up data model.
|
||||
$this->assertTrue($this->container->get('module_installer')->install(['node'], TRUE), 'Installed modules.');
|
||||
FieldStorageConfig::create([
|
||||
'entity_type' => static::$entityTypeId,
|
||||
'field_name' => 'field_favorite_animal',
|
||||
'type' => 'string',
|
||||
])
|
||||
->setCardinality(1)
|
||||
->save();
|
||||
FieldConfig::create([
|
||||
'entity_type' => static::$entityTypeId,
|
||||
'field_name' => 'field_favorite_animal',
|
||||
'bundle' => 'user',
|
||||
])
|
||||
->setLabel('Test field')
|
||||
->setTranslatable(FALSE)
|
||||
->save();
|
||||
$this->drupalCreateContentType(['type' => 'x']);
|
||||
$this->rebuildAll();
|
||||
$this->grantPermissionsToTestedRole(['access content']);
|
||||
|
||||
// Create data.
|
||||
$user_a = User::create([])->setUsername('A')->activate();
|
||||
$user_a->save();
|
||||
$user_b = User::create([])->setUsername('B')->set('field_favorite_animal', 'stegosaurus')->block();
|
||||
$user_b->save();
|
||||
$node_a = Node::create(['type' => 'x'])->setTitle('Owned by A')->setOwner($user_a);
|
||||
$node_a->save();
|
||||
$node_b = Node::create(['type' => 'x'])->setTitle('Owned by B')->setOwner($user_b);
|
||||
$node_b->save();
|
||||
$node_anon_1 = Node::create(['type' => 'x'])->setTitle('Owned by anon #1')->setOwnerId(0);
|
||||
$node_anon_1->save();
|
||||
$node_anon_2 = Node::create(['type' => 'x'])->setTitle('Owned by anon #2')->setOwnerId(0);
|
||||
$node_anon_2->save();
|
||||
$node_auth_1 = Node::create(['type' => 'x'])->setTitle('Owned by auth #1')->setOwner($this->account);
|
||||
$node_auth_1->save();
|
||||
|
||||
$favorite_animal_test_url = Url::fromRoute('jsonapi.user--user.collection')->setOption('query', ['filter[field_favorite_animal]' => 'stegosaurus']);
|
||||
|
||||
// Test.
|
||||
$collection_url = Url::fromRoute('jsonapi.node--x.collection');
|
||||
$request_options = [];
|
||||
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
|
||||
$request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions());
|
||||
// ?filter[uid.id]=OWN_UUID requires no permissions: 1 result.
|
||||
$response = $this->request('GET', $collection_url->setOption('query', ['filter[uid.id]' => $this->account->uuid()]), $request_options);
|
||||
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Contexts', 'user.permissions');
|
||||
$doc = $this->getDocumentFromResponse($response);
|
||||
$this->assertCount(1, $doc['data']);
|
||||
$this->assertSame($node_auth_1->uuid(), $doc['data'][0]['id']);
|
||||
// ?filter[uid.id]=ANONYMOUS_UUID: 0 results.
|
||||
$response = $this->request('GET', $collection_url->setOption('query', ['filter[uid.id]' => User::load(0)->uuid()]), $request_options);
|
||||
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Contexts', 'user.permissions');
|
||||
$doc = $this->getDocumentFromResponse($response);
|
||||
$this->assertCount(0, $doc['data']);
|
||||
// ?filter[uid.name]=A: 0 results.
|
||||
$response = $this->request('GET', $collection_url->setOption('query', ['filter[uid.name]' => 'A']), $request_options);
|
||||
$doc = $this->getDocumentFromResponse($response);
|
||||
$this->assertCount(0, $doc['data']);
|
||||
// /jsonapi/user/user?filter[field_favorite_animal]: 0 results.
|
||||
$response = $this->request('GET', $favorite_animal_test_url, $request_options);
|
||||
$doc = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
$this->assertCount(0, $doc['data']);
|
||||
// Grant "view" permission.
|
||||
$this->grantPermissionsToTestedRole(['access user profiles']);
|
||||
// ?filter[uid.id]=ANONYMOUS_UUID: 0 results.
|
||||
$response = $this->request('GET', $collection_url->setOption('query', ['filter[uid.id]' => User::load(0)->uuid()]), $request_options);
|
||||
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Contexts', 'user.permissions');
|
||||
$doc = $this->getDocumentFromResponse($response);
|
||||
$this->assertCount(0, $doc['data']);
|
||||
// ?filter[uid.name]=A: 1 result since user A is active.
|
||||
$response = $this->request('GET', $collection_url->setOption('query', ['filter[uid.name]' => 'A']), $request_options);
|
||||
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Contexts', 'user.permissions');
|
||||
$doc = $this->getDocumentFromResponse($response);
|
||||
$this->assertCount(1, $doc['data']);
|
||||
$this->assertSame($node_a->uuid(), $doc['data'][0]['id']);
|
||||
// ?filter[uid.name]=B: 0 results since user B is blocked.
|
||||
$response = $this->request('GET', $collection_url->setOption('query', ['filter[uid.name]' => 'B']), $request_options);
|
||||
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Contexts', 'user.permissions');
|
||||
$doc = $this->getDocumentFromResponse($response);
|
||||
$this->assertCount(0, $doc['data']);
|
||||
// /jsonapi/user/user?filter[field_favorite_animal]: 0 results.
|
||||
$response = $this->request('GET', $favorite_animal_test_url, $request_options);
|
||||
$doc = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
$this->assertCount(0, $doc['data']);
|
||||
// Grant "admin" permission.
|
||||
$this->grantPermissionsToTestedRole(['administer users']);
|
||||
// ?filter[uid.name]=B: 1 result.
|
||||
$response = $this->request('GET', $collection_url->setOption('query', ['filter[uid.name]' => 'B']), $request_options);
|
||||
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Contexts', 'user.permissions');
|
||||
$doc = $this->getDocumentFromResponse($response);
|
||||
$this->assertCount(1, $doc['data']);
|
||||
$this->assertSame($node_b->uuid(), $doc['data'][0]['id']);
|
||||
// /jsonapi/user/user?filter[field_favorite_animal]: 1 result.
|
||||
$response = $this->request('GET', $favorite_animal_test_url, $request_options);
|
||||
$doc = $this->getDocumentFromResponse($response);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
$this->assertCount(1, $doc['data']);
|
||||
$this->assertSame($user_b->uuid(), $doc['data'][0]['id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests users with altered display names.
|
||||
*/
|
||||
public function testResaveAccountName(): void {
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
$this->setUpAuthorization('PATCH');
|
||||
|
||||
$original_name = $this->entity->get('name')->value;
|
||||
|
||||
$url = Url::fromRoute('jsonapi.user--user.individual', ['entity' => $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);
|
||||
|
||||
// Send the unchanged data back.
|
||||
$request_options[RequestOptions::BODY] = (string) $response->getBody();
|
||||
$request_options[RequestOptions::HEADERS]['Content-Type'] = 'application/vnd.api+json';
|
||||
$response = $this->request('PATCH', $url, $request_options);
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
|
||||
// Load the user entity again, make sure the name was not changed.
|
||||
$this->entityStorage->resetCache();
|
||||
$updated_user = $this->entityStorage->load($this->entity->id());
|
||||
$this->assertEquals($original_name, $updated_user->get('name')->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if JSON:API respects user.settings.cancel_method: user_cancel_block.
|
||||
*/
|
||||
public function testDeleteRespectsUserCancelBlock(): void {
|
||||
$cancel_method = 'user_cancel_block';
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
$this->config('user.settings')->set('cancel_method', $cancel_method)->save(TRUE);
|
||||
|
||||
$account = $this->createAnotherEntity($cancel_method);
|
||||
$node = $this->drupalCreateNode(['uid' => $account->id()]);
|
||||
|
||||
$this->sendDeleteRequestForUser($account, $cancel_method);
|
||||
|
||||
$user_storage = $this->container->get('entity_type.manager')
|
||||
->getStorage('user');
|
||||
$user_storage->resetCache([$account->id()]);
|
||||
$account = $user_storage->load($account->id());
|
||||
|
||||
$this->assertNotNull($account, 'User is not deleted after JSON:API DELETE operation with user.settings.cancel_method: ' . $cancel_method);
|
||||
$this->assertTrue($account->isBlocked(), 'User is blocked after JSON:API DELETE operation with user.settings.cancel_method: ' . $cancel_method);
|
||||
|
||||
$node_storage = $this->container->get('entity_type.manager')->getStorage('node');
|
||||
$node_storage->resetCache([$node->id()]);
|
||||
$test_node = $node_storage->load($node->id());
|
||||
$this->assertNotNull($test_node, 'Node of the user is not deleted.');
|
||||
$this->assertTrue($test_node->isPublished(), 'Node of the user is published.');
|
||||
$test_node = $node_storage->loadRevision($node->getRevisionId());
|
||||
$this->assertTrue($test_node->isPublished(), 'Node revision of the user is published.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if JSON:API respects user.settings.cancel_method: user_cancel_block_unpublish.
|
||||
*/
|
||||
public function testDeleteRespectsUserCancelBlockUnpublish(): void {
|
||||
$cancel_method = 'user_cancel_block_unpublish';
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
$this->config('user.settings')->set('cancel_method', $cancel_method)->save(TRUE);
|
||||
|
||||
$account = $this->createAnotherEntity($cancel_method);
|
||||
$node = $this->drupalCreateNode(['uid' => $account->id()]);
|
||||
|
||||
$this->sendDeleteRequestForUser($account, $cancel_method);
|
||||
|
||||
$user_storage = $this->container->get('entity_type.manager')
|
||||
->getStorage('user');
|
||||
$user_storage->resetCache([$account->id()]);
|
||||
$account = $user_storage->load($account->id());
|
||||
|
||||
$this->assertNotNull($account, 'User is not deleted after JSON:API DELETE operation with user.settings.cancel_method: ' . $cancel_method);
|
||||
$this->assertTrue($account->isBlocked(), 'User is blocked after JSON:API DELETE operation with user.settings.cancel_method: ' . $cancel_method);
|
||||
|
||||
$node_storage = $this->container->get('entity_type.manager')->getStorage('node');
|
||||
$node_storage->resetCache([$node->id()]);
|
||||
$test_node = $node_storage->load($node->id());
|
||||
$this->assertNotNull($test_node, 'Node of the user is not deleted.');
|
||||
$this->assertFalse($test_node->isPublished(), 'Node of the user is no longer published.');
|
||||
$test_node = $node_storage->loadRevision($node->getRevisionId());
|
||||
$this->assertFalse($test_node->isPublished(), 'Node revision of the user is no longer published.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if JSON:API respects user.settings.cancel_method: user_cancel_block_unpublish.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
public function testDeleteRespectsUserCancelBlockUnpublishAndProcessesBatches(): void {
|
||||
$cancel_method = 'user_cancel_block_unpublish';
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
$this->config('user.settings')->set('cancel_method', $cancel_method)->save(TRUE);
|
||||
|
||||
$account = $this->createAnotherEntity($cancel_method);
|
||||
|
||||
$nodeCount = self::BATCH_TEST_NODE_COUNT;
|
||||
$node_ids = [];
|
||||
$nodes = [];
|
||||
while ($nodeCount-- > 0) {
|
||||
$node = $this->drupalCreateNode(['uid' => $account->id()]);
|
||||
$nodes[] = $node;
|
||||
$node_ids[] = $node->id();
|
||||
}
|
||||
|
||||
$this->sendDeleteRequestForUser($account, $cancel_method);
|
||||
|
||||
$user_storage = $this->container->get('entity_type.manager')
|
||||
->getStorage('user');
|
||||
$user_storage->resetCache([$account->id()]);
|
||||
$account = $user_storage->load($account->id());
|
||||
|
||||
$this->assertNotNull($account, 'User is not deleted after JSON:API DELETE operation with user.settings.cancel_method: ' . $cancel_method);
|
||||
$this->assertTrue($account->isBlocked(), 'User is blocked after JSON:API DELETE operation with user.settings.cancel_method: ' . $cancel_method);
|
||||
|
||||
$node_storage = $this->container->get('entity_type.manager')->getStorage('node');
|
||||
$node_storage->resetCache($node_ids);
|
||||
|
||||
$test_nodes = $node_storage->loadMultiple($node_ids);
|
||||
|
||||
$this->assertCount(self::BATCH_TEST_NODE_COUNT, $test_nodes, 'Nodes of the user are not deleted.');
|
||||
|
||||
foreach ($test_nodes as $test_node) {
|
||||
$this->assertFalse($test_node->isPublished(), 'Node of the user is no longer published.');
|
||||
}
|
||||
|
||||
foreach ($nodes as $node) {
|
||||
$test_node = $node_storage->loadRevision($node->getRevisionId());
|
||||
$this->assertFalse($test_node->isPublished(), 'Node revision of the user is no longer published.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if JSON:API respects user.settings.cancel_method: user_cancel_reassign.
|
||||
*/
|
||||
public function testDeleteRespectsUserCancelReassign(): void {
|
||||
$cancel_method = 'user_cancel_reassign';
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
$this->config('user.settings')->set('cancel_method', $cancel_method)->save(TRUE);
|
||||
|
||||
$account = $this->createAnotherEntity($cancel_method);
|
||||
$node = $this->drupalCreateNode(['uid' => $account->id()]);
|
||||
|
||||
$this->sendDeleteRequestForUser($account, $cancel_method);
|
||||
|
||||
$user_storage = $this->container->get('entity_type.manager')
|
||||
->getStorage('user');
|
||||
$user_storage->resetCache([$account->id()]);
|
||||
$account = $user_storage->load($account->id());
|
||||
|
||||
$this->assertNull($account, 'User is deleted after JSON:API DELETE operation with user.settings.cancel_method: ' . $cancel_method);
|
||||
|
||||
$node_storage = $this->container->get('entity_type.manager')->getStorage('node');
|
||||
$node_storage->resetCache([$node->id()]);
|
||||
$test_node = $node_storage->load($node->id());
|
||||
$this->assertNotNull($test_node, 'Node of the user is not deleted.');
|
||||
$this->assertTrue($test_node->isPublished(), 'Node of the user is still published.');
|
||||
$this->assertEquals(0, $test_node->getOwnerId(), 'Node of the user has been attributed to anonymous user.');
|
||||
$test_node = $node_storage->loadRevision($node->getRevisionId());
|
||||
$this->assertTrue($test_node->isPublished(), 'Node revision of the user is still published.');
|
||||
$this->assertEquals(0, $test_node->getRevisionUser()->id(), 'Node revision of the user has been attributed to anonymous user.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if JSON:API respects user.settings.cancel_method: user_cancel_delete.
|
||||
*/
|
||||
public function testDeleteRespectsUserCancelDelete(): void {
|
||||
$cancel_method = 'user_cancel_delete';
|
||||
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
|
||||
$this->config('user.settings')->set('cancel_method', $cancel_method)->save(TRUE);
|
||||
|
||||
$account = $this->createAnotherEntity($cancel_method);
|
||||
$node = $this->drupalCreateNode(['uid' => $account->id()]);
|
||||
|
||||
$url = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), ['entity' => $account->uuid()]);
|
||||
$request_options = [];
|
||||
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
|
||||
$request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions());
|
||||
$this->setUpAuthorization('DELETE');
|
||||
$response = $this->request('DELETE', $url, $request_options);
|
||||
$this->assertResourceResponse(204, NULL, $response);
|
||||
|
||||
$node_storage = $this->container->get('entity_type.manager')->getStorage('node');
|
||||
$user_storage = $this->container->get('entity_type.manager')->getStorage('user');
|
||||
|
||||
$user_storage->resetCache([$account->id()]);
|
||||
$account = $user_storage->load($account->id());
|
||||
$this->assertNull($account, 'User is deleted after JSON:API DELETE operation with user.settings.cancel_method: ' . $cancel_method);
|
||||
|
||||
$node_storage->resetCache([$node->id()]);
|
||||
$test_node = $node_storage->load($node->id());
|
||||
$this->assertNull($test_node, 'Node of the user is deleted.');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getModifiedEntityForPostTesting() {
|
||||
$modified = parent::getModifiedEntityForPostTesting();
|
||||
$modified['data']['attributes']['name'] = $this->randomMachineName();
|
||||
return $modified;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function makeNormalizationInvalid(array $document, $entity_key) {
|
||||
if ($entity_key === 'label') {
|
||||
$document['data']['attributes']['name'] = [
|
||||
0 => $document['data']['attributes']['name'],
|
||||
1 => 'Second Title',
|
||||
];
|
||||
return $document;
|
||||
}
|
||||
return parent::makeNormalizationInvalid($document, $entity_key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Drupal\user\UserInterface $account
|
||||
* The user account.
|
||||
* @param string $cancel_method
|
||||
* The cancel method.
|
||||
*/
|
||||
private function sendDeleteRequestForUser(UserInterface $account, string $cancel_method): void {
|
||||
$url = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), ['entity' => $account->uuid()]);
|
||||
$request_options = [];
|
||||
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
|
||||
$request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions());
|
||||
$this->setUpAuthorization('DELETE');
|
||||
$response = $this->request('DELETE', $url, $request_options);
|
||||
$this->assertResourceResponse(204, NULL, $response);
|
||||
}
|
||||
|
||||
}
|
||||
130
web/core/modules/jsonapi/tests/src/Functional/ViewTest.php
Normal file
130
web/core/modules/jsonapi/tests/src/Functional/ViewTest.php
Normal file
@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\views\Entity\View;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "View" config entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class ViewTest extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['views', 'views_ui'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'view';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'view--view';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\views\ViewEntityInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['administer views']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
$view = View::create([
|
||||
'id' => 'test_rest',
|
||||
'label' => 'Test REST',
|
||||
]);
|
||||
$view->save();
|
||||
return $view;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/view/view/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'view--view',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'base_field' => 'nid',
|
||||
'base_table' => 'node',
|
||||
'dependencies' => [],
|
||||
'description' => '',
|
||||
'display' => [
|
||||
'default' => [
|
||||
'display_plugin' => 'default',
|
||||
'id' => 'default',
|
||||
'display_title' => 'Default',
|
||||
'position' => 0,
|
||||
'display_options' => [
|
||||
'display_extenders' => [],
|
||||
],
|
||||
'cache_metadata' => [
|
||||
'max-age' => -1,
|
||||
'contexts' => [
|
||||
'languages:language_interface',
|
||||
'url.query_args',
|
||||
],
|
||||
'tags' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
'label' => 'Test REST',
|
||||
'langcode' => 'en',
|
||||
'module' => 'views',
|
||||
'status' => TRUE,
|
||||
'tag' => '',
|
||||
'drupal_internal__id' => 'test_rest',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
// @todo Update in https://www.drupal.org/node/2300677.
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
120
web/core/modules/jsonapi/tests/src/Functional/VocabularyTest.php
Normal file
120
web/core/modules/jsonapi/tests/src/Functional/VocabularyTest.php
Normal file
@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\taxonomy\Entity\Vocabulary;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "vocabulary" config entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class VocabularyTest extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['taxonomy'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'taxonomy_vocabulary';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'taxonomy_vocabulary--taxonomy_vocabulary';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\taxonomy\VocabularyInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['administer taxonomy']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
$vocabulary = Vocabulary::create([
|
||||
'name' => 'Llama',
|
||||
'vid' => 'llama',
|
||||
]);
|
||||
$vocabulary->save();
|
||||
|
||||
return $vocabulary;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/taxonomy_vocabulary/taxonomy_vocabulary/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'taxonomy_vocabulary--taxonomy_vocabulary',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'langcode' => 'en',
|
||||
'status' => TRUE,
|
||||
'dependencies' => [],
|
||||
'name' => 'Llama',
|
||||
'new_revision' => FALSE,
|
||||
'description' => NULL,
|
||||
'weight' => 0,
|
||||
'drupal_internal__vid' => 'llama',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
// @todo Update in https://www.drupal.org/node/2300677.
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessMessage($method) {
|
||||
if ($method === 'GET') {
|
||||
return "The following permissions are required: 'access taxonomy overview' OR 'administer taxonomy'.";
|
||||
}
|
||||
return parent::getExpectedUnauthorizedAccessMessage($method);
|
||||
}
|
||||
|
||||
}
|
||||
135
web/core/modules/jsonapi/tests/src/Functional/WorkflowTest.php
Normal file
135
web/core/modules/jsonapi/tests/src/Functional/WorkflowTest.php
Normal file
@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "Workflow" config entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
*/
|
||||
class WorkflowTest extends ConfigEntityResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['workflows', 'workflow_type_test'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'workflow';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'workflow--workflow';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\shortcut\ShortcutSetInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
$this->grantPermissionsToTestedRole(['administer workflows']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
$workflow = Workflow::create([
|
||||
'id' => 'rest_workflow',
|
||||
'label' => 'REST Workflow',
|
||||
'type' => 'workflow_type_complex_test',
|
||||
]);
|
||||
$workflow
|
||||
->getTypePlugin()
|
||||
->addState('draft', 'Draft')
|
||||
->addState('published', 'Published');
|
||||
$configuration = $workflow->getTypePlugin()->getConfiguration();
|
||||
$configuration['example_setting'] = 'foo';
|
||||
$configuration['states']['draft']['extra'] = 'bar';
|
||||
$workflow->getTypePlugin()->setConfiguration($configuration);
|
||||
$workflow->save();
|
||||
return $workflow;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$self_url = Url::fromUri('base:/jsonapi/workflow/workflow/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
|
||||
return [
|
||||
'jsonapi' => [
|
||||
'meta' => [
|
||||
'links' => [
|
||||
'self' => ['href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => 'workflow--workflow',
|
||||
'links' => [
|
||||
'self' => ['href' => $self_url],
|
||||
],
|
||||
'attributes' => [
|
||||
'dependencies' => [
|
||||
'module' => [
|
||||
'workflow_type_test',
|
||||
],
|
||||
],
|
||||
'label' => 'REST Workflow',
|
||||
'langcode' => 'en',
|
||||
'status' => TRUE,
|
||||
'workflow_type' => 'workflow_type_complex_test',
|
||||
'type_settings' => [
|
||||
'states' => [
|
||||
'draft' => [
|
||||
'extra' => 'bar',
|
||||
'label' => 'Draft',
|
||||
'weight' => 0,
|
||||
],
|
||||
'published' => [
|
||||
'label' => 'Published',
|
||||
'weight' => 1,
|
||||
],
|
||||
],
|
||||
'transitions' => [],
|
||||
'example_setting' => 'foo',
|
||||
],
|
||||
'drupal_internal__id' => 'rest_workflow',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
// @todo Update in https://www.drupal.org/node/2300677.
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
265
web/core/modules/jsonapi/tests/src/Functional/WorkspaceTest.php
Normal file
265
web/core/modules/jsonapi/tests/src/Functional/WorkspaceTest.php
Normal file
@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Functional;
|
||||
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\user\Entity\User;
|
||||
use Drupal\workspaces\Entity\Workspace;
|
||||
|
||||
/**
|
||||
* JSON:API integration test for the "Workspace" content entity type.
|
||||
*
|
||||
* @group jsonapi
|
||||
* @group workspaces
|
||||
*/
|
||||
class WorkspaceTest extends ResourceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['workspaces'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'workspace';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeName = 'workspace--workspace';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $resourceTypeIsVersionable = TRUE;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $patchProtectedFieldNames = [
|
||||
'changed' => NULL,
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $uniqueFieldNames = ['id'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $firstCreatedEntityId = 'autumn_campaign';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $secondCreatedEntityId = 'autumn_campaign';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method): void {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
$this->grantPermissionsToTestedRole(['view any workspace']);
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
$this->grantPermissionsToTestedRole(['create workspace']);
|
||||
break;
|
||||
|
||||
case 'PATCH':
|
||||
$this->grantPermissionsToTestedRole(['edit any workspace']);
|
||||
break;
|
||||
|
||||
case 'DELETE':
|
||||
$this->grantPermissionsToTestedRole(['delete any workspace']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity(): EntityInterface {
|
||||
$entity = Workspace::create([
|
||||
'id' => 'campaign',
|
||||
'label' => 'Campaign',
|
||||
'uid' => $this->account->id(),
|
||||
'created' => 123456789,
|
||||
]);
|
||||
$entity->save();
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedDocument(): array {
|
||||
$author = User::load($this->entity->getOwnerId());
|
||||
$base_url = Url::fromUri('base:/jsonapi/workspace/workspace/' . $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' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK],
|
||||
],
|
||||
],
|
||||
'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => $base_url->toString()],
|
||||
],
|
||||
'data' => [
|
||||
'id' => $this->entity->uuid(),
|
||||
'type' => static::$resourceTypeName,
|
||||
'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),
|
||||
'label' => 'Campaign',
|
||||
'drupal_internal__id' => 'campaign',
|
||||
'drupal_internal__revision_id' => 1,
|
||||
],
|
||||
'relationships' => [
|
||||
'parent' => [
|
||||
'data' => NULL,
|
||||
'links' => [
|
||||
'related' => [
|
||||
'href' => $base_url->toString() . '/parent' . $version_query_string,
|
||||
],
|
||||
'self' => [
|
||||
'href' => $base_url->toString() . '/relationships/parent' . $version_query_string,
|
||||
],
|
||||
],
|
||||
],
|
||||
'uid' => [
|
||||
'data' => [
|
||||
'id' => $author->uuid(),
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => (int) $author->id(),
|
||||
],
|
||||
'type' => 'user--user',
|
||||
],
|
||||
'links' => [
|
||||
'related' => [
|
||||
'href' => $base_url->toString() . '/uid' . $version_query_string,
|
||||
],
|
||||
'self' => [
|
||||
'href' => $base_url->toString() . '/relationships/uid' . $version_query_string,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPostDocument(): array {
|
||||
return [
|
||||
'data' => [
|
||||
'type' => static::$resourceTypeName,
|
||||
'attributes' => [
|
||||
'drupal_internal__id' => 'autumn_campaign',
|
||||
'label' => 'Autumn campaign',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getModifiedEntityForPostTesting() {
|
||||
$modified = parent::getModifiedEntityForPostTesting();
|
||||
// Even though the field type of the workspace ID is 'string', it acts as a
|
||||
// machine name through a custom constraint, so we need to ensure that we
|
||||
// generate a proper random value for it.
|
||||
// @see \Drupal\workspaces\Entity\Workspace::baseFieldDefinitions()
|
||||
$modified['data']['attributes']['id'] = $this->randomMachineName();
|
||||
return $modified;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getPatchDocument(): array {
|
||||
$patch_document = parent::getPatchDocument();
|
||||
unset($patch_document['data']['attributes']['drupal_internal__id']);
|
||||
return $patch_document;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessCacheability(): CacheableMetadata {
|
||||
// @see \Drupal\workspaces\WorkspaceAccessControlHandler::checkAccess()
|
||||
return parent::getExpectedUnauthorizedAccessCacheability()
|
||||
->addCacheTags(['workspace:campaign'])
|
||||
// The "view|edit|delete own workspace" permissions add the 'user' cache
|
||||
// context.
|
||||
->addCacheContexts(['user']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessMessage($method): string {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
return "The 'view own workspace' permission is required.";
|
||||
|
||||
case 'POST':
|
||||
return "The following permissions are required: 'administer workspaces' OR 'create workspace'.";
|
||||
|
||||
case 'PATCH':
|
||||
return "The 'edit own workspace' permission is required.";
|
||||
|
||||
case 'DELETE':
|
||||
return "The 'delete own workspace' permission is required.";
|
||||
|
||||
default:
|
||||
return parent::getExpectedUnauthorizedAccessMessage($method);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getSparseFieldSets(): array {
|
||||
// Workspace's resource type name ('workspace') comes after the 'uid' field,
|
||||
// which breaks nested sparse fieldset tests.
|
||||
return array_diff_key(parent::getSparseFieldSets(), array_flip([
|
||||
'nested_empty_fieldset',
|
||||
'nested_fieldset_with_owner_fieldset',
|
||||
]));
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,249 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\FunctionalJavascript;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\FunctionalJavascriptTests\PerformanceTestBase;
|
||||
|
||||
/**
|
||||
* Tests performance for JSON:API routes.
|
||||
*
|
||||
* @group Common
|
||||
* @group #slow
|
||||
* @requires extension apcu
|
||||
*/
|
||||
class JsonApiPerformanceTest extends PerformanceTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['jsonapi', 'node'];
|
||||
|
||||
/**
|
||||
* Tests performance of the navigation toolbar.
|
||||
*/
|
||||
public function testGetIndividual(): void {
|
||||
|
||||
$this->drupalCreateContentType(['type' => 'article']);
|
||||
\Drupal::service('router.builder')->rebuildIfNeeded();
|
||||
$node = $this->drupalCreateNode([
|
||||
'type' => 'article',
|
||||
'title' => 'Example article',
|
||||
'uuid' => '677f9911-f002-4639-9891-5c39e8b00d9d',
|
||||
]);
|
||||
|
||||
$user = $this->drupalCreateUser();
|
||||
$user->addRole('administrator');
|
||||
$user->save();
|
||||
$this->drupalLogin($user);
|
||||
|
||||
// Request the front page to ensure all cache collectors are fully
|
||||
// warmed, wait one second to ensure that the request finished processing.
|
||||
$this->drupalGet('');
|
||||
sleep(1);
|
||||
|
||||
$url = Url::fromRoute('jsonapi.node--article.individual', ['entity' => $node->uuid()])->toString();
|
||||
$performance_data = $this->collectPerformanceData(function () use ($url) {
|
||||
$this->drupalGet($url);
|
||||
}, 'jsonapi_individual_cool_cache');
|
||||
|
||||
$expected_queries = [
|
||||
'SELECT "session" FROM "sessions" WHERE "sid" = "SESSION_ID" LIMIT 0, 1',
|
||||
'SELECT * FROM "users_field_data" "u" WHERE "u"."uid" = "2" AND "u"."default_langcode" = 1',
|
||||
'SELECT "roles_target_id" FROM "user__roles" WHERE "entity_id" = "2"',
|
||||
'SELECT "base_table"."id" AS "id", "base_table"."path" AS "path", "base_table"."alias" AS "alias", "base_table"."langcode" AS "langcode" FROM "path_alias" "base_table" WHERE ("base_table"."status" = 1) AND ("base_table"."alias" LIKE "/jsonapi/node/article/677f9911-f002-4639-9891-5c39e8b00d9d" ESCAPE ' . "'\\\\'" . ') AND ("base_table"."langcode" IN ("en", "und")) ORDER BY "base_table"."langcode" ASC, "base_table"."id" DESC',
|
||||
'SELECT "name", "route", "fit" FROM "router" WHERE "pattern_outline" IN ( "/jsonapi/node/article/677f9911-f002-4639-9891-5c39e8b00d9d", "/jsonapi/node/article/%", "/jsonapi/node/%/%", "/jsonapi/%/article/677f9911-f002-4639-9891-5c39e8b00d9d", "/jsonapi/%/%/%", "/jsonapi/node/article", "/jsonapi/node/%", "/jsonapi/%/article", "/jsonapi/node", "/jsonapi/%", "/jsonapi/node/article/%"0 ) AND "number_parts" >= 4',
|
||||
'SELECT "base_table"."vid" AS "vid", "base_table"."nid" AS "nid" FROM "node" "base_table" INNER JOIN "node" "node" ON "node"."nid" = "base_table"."nid" INNER JOIN "node_field_data" "node_field_data" ON "node_field_data"."nid" = "base_table"."nid" WHERE ("node"."uuid" IN ("677f9911-f002-4639-9891-5c39e8b00d9d")) AND ("node_field_data"."default_langcode" IN (1))',
|
||||
'SELECT "revision"."vid" AS "vid", "revision"."langcode" AS "langcode", "revision"."revision_uid" AS "revision_uid", "revision"."revision_timestamp" AS "revision_timestamp", "revision"."revision_log" AS "revision_log", "revision"."revision_default" AS "revision_default", "base"."nid" AS "nid", "base"."type" AS "type", "base"."uuid" AS "uuid", CASE "base"."vid" WHEN "revision"."vid" THEN 1 ELSE 0 END AS "isDefaultRevision" FROM "node" "base" INNER JOIN "node_revision" "revision" ON "revision"."vid" = "base"."vid" WHERE "base"."nid" IN (1)',
|
||||
'SELECT "revision".* FROM "node_field_revision" "revision" WHERE ("revision"."nid" IN (1)) AND ("revision"."vid" IN ("1")) ORDER BY "revision"."nid" ASC',
|
||||
'SELECT "t".* FROM "node__body" "t" WHERE ("entity_id" IN (1)) AND ("deleted" = 0) AND ("langcode" IN ("en", "und", "zxx")) ORDER BY "delta" ASC',
|
||||
'SELECT 1 AS "expression" FROM "path_alias" "base_table" WHERE ("base_table"."status" = 1) AND ("base_table"."path" LIKE "/jsonapi%" ESCAPE ' . "'\\\\'" . ') LIMIT 1 OFFSET 0',
|
||||
'SELECT "base_table"."vid" AS "vid", "base_table"."nid" AS "nid" FROM "node_revision" "base_table" INNER JOIN "node_field_data" "node_field_data" ON "node_field_data"."nid" = "base_table"."nid" WHERE ("base_table"."vid" IN (SELECT MAX(base_table.vid) AS "expression" FROM "node_revision" "base_table" GROUP BY "base_table"."nid")) AND ("node_field_data"."nid" = "1")',
|
||||
'SELECT "name", "route" FROM "router" WHERE "name" IN ( "jsonapi.node--article.node_type.relationship.get" )',
|
||||
'SELECT "name", "route" FROM "router" WHERE "name" IN ( "jsonapi.node--article.node_type.related" )',
|
||||
'SELECT "base_table"."vid" AS "vid", "base_table"."nid" AS "nid" FROM "node" "base_table" INNER JOIN "node" "node" ON "node"."nid" = "base_table"."nid" INNER JOIN "node_field_data" "node_field_data" ON "node_field_data"."nid" = "base_table"."nid" WHERE ("node"."uuid" IN ("677f9911-f002-4639-9891-5c39e8b00d9d")) AND ("node_field_data"."default_langcode" IN (1))',
|
||||
'SELECT "base_table"."vid" AS "vid", "base_table"."nid" AS "nid" FROM "node" "base_table" INNER JOIN "node" "node" ON "node"."nid" = "base_table"."nid" INNER JOIN "node_field_data" "node_field_data" ON "node_field_data"."nid" = "base_table"."nid" WHERE ("node"."uuid" IN ("677f9911-f002-4639-9891-5c39e8b00d9d")) AND ("node_field_data"."default_langcode" IN (1))',
|
||||
'SELECT "name", "route" FROM "router" WHERE "name" IN ( "jsonapi.node--article.revision_uid.relationship.get" )',
|
||||
'SELECT "name", "route" FROM "router" WHERE "name" IN ( "jsonapi.node--article.revision_uid.related" )',
|
||||
'SELECT "base_table"."vid" AS "vid", "base_table"."nid" AS "nid" FROM "node" "base_table" INNER JOIN "node" "node" ON "node"."nid" = "base_table"."nid" INNER JOIN "node_field_data" "node_field_data" ON "node_field_data"."nid" = "base_table"."nid" WHERE ("node"."uuid" IN ("677f9911-f002-4639-9891-5c39e8b00d9d")) AND ("node_field_data"."default_langcode" IN (1))',
|
||||
'SELECT "base_table"."vid" AS "vid", "base_table"."nid" AS "nid" FROM "node" "base_table" INNER JOIN "node" "node" ON "node"."nid" = "base_table"."nid" INNER JOIN "node_field_data" "node_field_data" ON "node_field_data"."nid" = "base_table"."nid" WHERE ("node"."uuid" IN ("677f9911-f002-4639-9891-5c39e8b00d9d")) AND ("node_field_data"."default_langcode" IN (1))',
|
||||
'SELECT "name", "route" FROM "router" WHERE "name" IN ( "jsonapi.node--article.uid.relationship.get" )',
|
||||
'SELECT "name", "route" FROM "router" WHERE "name" IN ( "jsonapi.node--article.uid.related" )',
|
||||
'SELECT "base_table"."vid" AS "vid", "base_table"."nid" AS "nid" FROM "node" "base_table" INNER JOIN "node" "node" ON "node"."nid" = "base_table"."nid" INNER JOIN "node_field_data" "node_field_data" ON "node_field_data"."nid" = "base_table"."nid" WHERE ("node"."uuid" IN ("677f9911-f002-4639-9891-5c39e8b00d9d")) AND ("node_field_data"."default_langcode" IN (1))',
|
||||
'SELECT "base_table"."vid" AS "vid", "base_table"."nid" AS "nid" FROM "node" "base_table" INNER JOIN "node" "node" ON "node"."nid" = "base_table"."nid" INNER JOIN "node_field_data" "node_field_data" ON "node_field_data"."nid" = "base_table"."nid" WHERE ("node"."uuid" IN ("677f9911-f002-4639-9891-5c39e8b00d9d")) AND ("node_field_data"."default_langcode" IN (1))',
|
||||
'SELECT "base_table"."vid" AS "vid", "base_table"."nid" AS "nid" FROM "node" "base_table" INNER JOIN "node" "node" ON "node"."nid" = "base_table"."nid" INNER JOIN "node_field_data" "node_field_data" ON "node_field_data"."nid" = "base_table"."nid" WHERE ("node"."uuid" IN ("677f9911-f002-4639-9891-5c39e8b00d9d")) AND ("node_field_data"."default_langcode" IN (1))',
|
||||
'INSERT INTO "semaphore" ("name", "value", "expire") VALUES ("path_alias_prefix_list:Drupal\Core\Cache\CacheCollector", "LOCK_ID", "EXPIRE")',
|
||||
'DELETE FROM "semaphore" WHERE ("name" = "path_alias_prefix_list:Drupal\Core\Cache\CacheCollector") AND ("value" = "LOCK_ID")',
|
||||
];
|
||||
$recorded_queries = $performance_data->getQueries();
|
||||
$this->assertSame($expected_queries, $recorded_queries);
|
||||
|
||||
$expected = [
|
||||
'QueryCount' => 26,
|
||||
'CacheGetCount' => 42,
|
||||
'CacheGetCountByBin' => [
|
||||
'config' => 8,
|
||||
'data' => 8,
|
||||
'bootstrap' => 5,
|
||||
'discovery' => 13,
|
||||
'entity' => 2,
|
||||
'default' => 4,
|
||||
'dynamic_page_cache' => 1,
|
||||
'jsonapi_normalizations' => 1,
|
||||
],
|
||||
'CacheSetCount' => 16,
|
||||
'CacheSetCountByBin' => [
|
||||
'data' => 7,
|
||||
'entity' => 1,
|
||||
'default' => 3,
|
||||
'dynamic_page_cache' => 2,
|
||||
'jsonapi_normalizations' => 2,
|
||||
'bootstrap' => 1,
|
||||
],
|
||||
'CacheDeleteCount' => 0,
|
||||
'CacheTagInvalidationCount' => 0,
|
||||
'CacheTagLookupQueryCount' => 3,
|
||||
'CacheTagGroupedLookups' => [
|
||||
[
|
||||
'route_match',
|
||||
'access_policies',
|
||||
'routes',
|
||||
'router',
|
||||
'entity_types',
|
||||
'entity_field_info',
|
||||
'entity_bundles',
|
||||
'local_task',
|
||||
'library_info',
|
||||
],
|
||||
['jsonapi_resource_types'],
|
||||
['config:filter.format.plain_text', 'http_response', 'node:1'],
|
||||
],
|
||||
];
|
||||
$this->assertMetrics($expected, $performance_data);
|
||||
|
||||
$url = Url::fromRoute('jsonapi.node--article.individual', ['entity' => $node->uuid()])->toString();
|
||||
$performance_data = $this->collectPerformanceData(function () use ($url) {
|
||||
$this->drupalGet($url);
|
||||
}, 'jsonapi_individual_hot_cache');
|
||||
|
||||
$expected_queries = [
|
||||
'SELECT "session" FROM "sessions" WHERE "sid" = "SESSION_ID" LIMIT 0, 1',
|
||||
'SELECT * FROM "users_field_data" "u" WHERE "u"."uid" = "2" AND "u"."default_langcode" = 1',
|
||||
'SELECT "roles_target_id" FROM "user__roles" WHERE "entity_id" = "2"',
|
||||
'SELECT "base_table"."vid" AS "vid", "base_table"."nid" AS "nid" FROM "node" "base_table" INNER JOIN "node" "node" ON "node"."nid" = "base_table"."nid" INNER JOIN "node_field_data" "node_field_data" ON "node_field_data"."nid" = "base_table"."nid" WHERE ("node"."uuid" IN ("677f9911-f002-4639-9891-5c39e8b00d9d")) AND ("node_field_data"."default_langcode" IN (1))',
|
||||
];
|
||||
$recorded_queries = $performance_data->getQueries();
|
||||
$this->assertSame($expected_queries, $recorded_queries);
|
||||
|
||||
$expected = [
|
||||
'QueryCount' => 4,
|
||||
'CacheGetCount' => 19,
|
||||
'CacheGetCountByBin' => [
|
||||
'config' => 6,
|
||||
'data' => 1,
|
||||
'discovery' => 5,
|
||||
'entity' => 1,
|
||||
'default' => 1,
|
||||
'bootstrap' => 3,
|
||||
'dynamic_page_cache' => 2,
|
||||
],
|
||||
'CacheSetCount' => 0,
|
||||
'CacheDeleteCount' => 0,
|
||||
'CacheTagInvalidationCount' => 0,
|
||||
'CacheTagLookupQueryCount' => 3,
|
||||
'CacheTagGroupedLookups' => [
|
||||
[
|
||||
'route_match',
|
||||
'access_policies',
|
||||
'routes',
|
||||
'router',
|
||||
'entity_types',
|
||||
'entity_field_info',
|
||||
'entity_bundles',
|
||||
'local_task',
|
||||
'library_info',
|
||||
],
|
||||
['jsonapi_resource_types'],
|
||||
['config:filter.format.plain_text', 'http_response', 'node:1'],
|
||||
],
|
||||
];
|
||||
$this->assertMetrics($expected, $performance_data);
|
||||
$this->assertSame(['jsonapi.resource_types'], $performance_data->getCacheOperations()['get']['default']);
|
||||
|
||||
$node->save();
|
||||
|
||||
$url = Url::fromRoute('jsonapi.node--article.individual', ['entity' => $node->uuid()])->toString();
|
||||
$performance_data = $this->collectPerformanceData(function () use ($url) {
|
||||
$this->drupalGet($url);
|
||||
}, 'jsonapi_node_individual_invalidated');
|
||||
|
||||
$expected_queries = [
|
||||
'SELECT "session" FROM "sessions" WHERE "sid" = "SESSION_ID" LIMIT 0, 1',
|
||||
'SELECT * FROM "users_field_data" "u" WHERE "u"."uid" = "2" AND "u"."default_langcode" = 1',
|
||||
'SELECT "roles_target_id" FROM "user__roles" WHERE "entity_id" = "2"',
|
||||
'SELECT "base_table"."vid" AS "vid", "base_table"."nid" AS "nid" FROM "node" "base_table" INNER JOIN "node" "node" ON "node"."nid" = "base_table"."nid" INNER JOIN "node_field_data" "node_field_data" ON "node_field_data"."nid" = "base_table"."nid" WHERE ("node"."uuid" IN ("677f9911-f002-4639-9891-5c39e8b00d9d")) AND ("node_field_data"."default_langcode" IN (1))',
|
||||
'SELECT "revision"."vid" AS "vid", "revision"."langcode" AS "langcode", "revision"."revision_uid" AS "revision_uid", "revision"."revision_timestamp" AS "revision_timestamp", "revision"."revision_log" AS "revision_log", "revision"."revision_default" AS "revision_default", "base"."nid" AS "nid", "base"."type" AS "type", "base"."uuid" AS "uuid", CASE "base"."vid" WHEN "revision"."vid" THEN 1 ELSE 0 END AS "isDefaultRevision" FROM "node" "base" INNER JOIN "node_revision" "revision" ON "revision"."vid" = "base"."vid" WHERE "base"."nid" IN (1)',
|
||||
'SELECT "revision".* FROM "node_field_revision" "revision" WHERE ("revision"."nid" IN (1)) AND ("revision"."vid" IN ("1")) ORDER BY "revision"."nid" ASC',
|
||||
'SELECT "t".* FROM "node__body" "t" WHERE ("entity_id" IN (1)) AND ("deleted" = 0) AND ("langcode" IN ("en", "und", "zxx")) ORDER BY "delta" ASC',
|
||||
'SELECT "base_table"."vid" AS "vid", "base_table"."nid" AS "nid" FROM "node_revision" "base_table" INNER JOIN "node_field_data" "node_field_data" ON "node_field_data"."nid" = "base_table"."nid" WHERE ("base_table"."vid" IN (SELECT MAX(base_table.vid) AS "expression" FROM "node_revision" "base_table" GROUP BY "base_table"."nid")) AND ("node_field_data"."nid" = "1")',
|
||||
'SELECT "base_table"."vid" AS "vid", "base_table"."nid" AS "nid" FROM "node" "base_table" INNER JOIN "node" "node" ON "node"."nid" = "base_table"."nid" INNER JOIN "node_field_data" "node_field_data" ON "node_field_data"."nid" = "base_table"."nid" WHERE ("node"."uuid" IN ("677f9911-f002-4639-9891-5c39e8b00d9d")) AND ("node_field_data"."default_langcode" IN (1))',
|
||||
'SELECT "base_table"."vid" AS "vid", "base_table"."nid" AS "nid" FROM "node" "base_table" INNER JOIN "node" "node" ON "node"."nid" = "base_table"."nid" INNER JOIN "node_field_data" "node_field_data" ON "node_field_data"."nid" = "base_table"."nid" WHERE ("node"."uuid" IN ("677f9911-f002-4639-9891-5c39e8b00d9d")) AND ("node_field_data"."default_langcode" IN (1))',
|
||||
'SELECT "base_table"."vid" AS "vid", "base_table"."nid" AS "nid" FROM "node" "base_table" INNER JOIN "node" "node" ON "node"."nid" = "base_table"."nid" INNER JOIN "node_field_data" "node_field_data" ON "node_field_data"."nid" = "base_table"."nid" WHERE ("node"."uuid" IN ("677f9911-f002-4639-9891-5c39e8b00d9d")) AND ("node_field_data"."default_langcode" IN (1))',
|
||||
'SELECT "base_table"."vid" AS "vid", "base_table"."nid" AS "nid" FROM "node" "base_table" INNER JOIN "node" "node" ON "node"."nid" = "base_table"."nid" INNER JOIN "node_field_data" "node_field_data" ON "node_field_data"."nid" = "base_table"."nid" WHERE ("node"."uuid" IN ("677f9911-f002-4639-9891-5c39e8b00d9d")) AND ("node_field_data"."default_langcode" IN (1))',
|
||||
'SELECT "base_table"."vid" AS "vid", "base_table"."nid" AS "nid" FROM "node" "base_table" INNER JOIN "node" "node" ON "node"."nid" = "base_table"."nid" INNER JOIN "node_field_data" "node_field_data" ON "node_field_data"."nid" = "base_table"."nid" WHERE ("node"."uuid" IN ("677f9911-f002-4639-9891-5c39e8b00d9d")) AND ("node_field_data"."default_langcode" IN (1))',
|
||||
'SELECT "base_table"."vid" AS "vid", "base_table"."nid" AS "nid" FROM "node" "base_table" INNER JOIN "node" "node" ON "node"."nid" = "base_table"."nid" INNER JOIN "node_field_data" "node_field_data" ON "node_field_data"."nid" = "base_table"."nid" WHERE ("node"."uuid" IN ("677f9911-f002-4639-9891-5c39e8b00d9d")) AND ("node_field_data"."default_langcode" IN (1))',
|
||||
'SELECT "base_table"."vid" AS "vid", "base_table"."nid" AS "nid" FROM "node" "base_table" INNER JOIN "node" "node" ON "node"."nid" = "base_table"."nid" INNER JOIN "node_field_data" "node_field_data" ON "node_field_data"."nid" = "base_table"."nid" WHERE ("node"."uuid" IN ("677f9911-f002-4639-9891-5c39e8b00d9d")) AND ("node_field_data"."default_langcode" IN (1))',
|
||||
];
|
||||
$recorded_queries = $performance_data->getQueries();
|
||||
$this->assertSame($expected_queries, $recorded_queries);
|
||||
|
||||
$expected = [
|
||||
'QueryCount' => 15,
|
||||
'CacheGetCount' => 43,
|
||||
'CacheGetCountByBin' => [
|
||||
'config' => 8,
|
||||
'data' => 8,
|
||||
'discovery' => 13,
|
||||
'entity' => 2,
|
||||
'default' => 4,
|
||||
'bootstrap' => 4,
|
||||
'dynamic_page_cache' => 2,
|
||||
'jsonapi_normalizations' => 2,
|
||||
],
|
||||
'CacheSetCount' => 3,
|
||||
'CacheDeleteCount' => 0,
|
||||
'CacheTagInvalidationCount' => 0,
|
||||
'CacheTagLookupQueryCount' => 3,
|
||||
'CacheTagGroupedLookups' => [
|
||||
[
|
||||
'route_match',
|
||||
'access_policies',
|
||||
'routes',
|
||||
'router',
|
||||
'entity_types',
|
||||
'entity_field_info',
|
||||
'entity_bundles',
|
||||
'local_task',
|
||||
'library_info',
|
||||
],
|
||||
['jsonapi_resource_types'],
|
||||
['config:filter.format.plain_text', 'http_response', 'node:1'],
|
||||
],
|
||||
];
|
||||
$this->assertMetrics($expected, $performance_data);
|
||||
$this->assertSame([
|
||||
'jsonapi.resource_types',
|
||||
'jsonapi.resource_type.node.article',
|
||||
'jsonapi.resource_type.node_type.node_type',
|
||||
'jsonapi.resource_type.user.user',
|
||||
], $performance_data->getCacheOperations()['get']['default']);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,456 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Kernel\Context;
|
||||
|
||||
use Drupal\Core\Http\Exception\CacheableBadRequestHttpException;
|
||||
use Drupal\entity_test\Entity\EntityTestBundle;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\Tests\jsonapi\Kernel\JsonapiKernelTestBase;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\jsonapi\Context\FieldResolver
|
||||
* @group jsonapi
|
||||
* @group #slow
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class FieldResolverTest extends JsonapiKernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'entity_test',
|
||||
'field',
|
||||
'file',
|
||||
'jsonapi_test_field_aliasing',
|
||||
'jsonapi_test_field_filter_access',
|
||||
'serialization',
|
||||
'text',
|
||||
'user',
|
||||
];
|
||||
|
||||
/**
|
||||
* The subject under test.
|
||||
*
|
||||
* @var \Drupal\jsonapi\Context\FieldResolver
|
||||
*/
|
||||
protected $sut;
|
||||
|
||||
/**
|
||||
* The JSON:API resource type repository.
|
||||
*
|
||||
* @var \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface
|
||||
*/
|
||||
protected $resourceTypeRepository;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->installEntitySchema('entity_test_with_bundle');
|
||||
$this->sut = \Drupal::service('jsonapi.field_resolver');
|
||||
|
||||
$this->makeBundle('bundle1');
|
||||
$this->makeBundle('bundle2');
|
||||
$this->makeBundle('bundle3');
|
||||
|
||||
$this->makeField('string', 'field_test1', 'entity_test_with_bundle', ['bundle1']);
|
||||
$this->makeField('string', 'field_test2', 'entity_test_with_bundle', ['bundle1']);
|
||||
$this->makeField('string', 'field_test3', 'entity_test_with_bundle', ['bundle2', 'bundle3']);
|
||||
|
||||
// Provides entity reference fields.
|
||||
$settings = ['target_type' => 'entity_test_with_bundle'];
|
||||
$this->makeField('entity_reference', 'field_test_ref1', 'entity_test_with_bundle', ['bundle1'], $settings, [
|
||||
'handler_settings' => [
|
||||
'target_bundles' => ['bundle2', 'bundle3'],
|
||||
],
|
||||
]);
|
||||
$this->makeField('entity_reference', 'field_test_ref2', 'entity_test_with_bundle', ['bundle1'], $settings);
|
||||
$this->makeField('entity_reference', 'field_test_ref3', 'entity_test_with_bundle', ['bundle2', 'bundle3'], $settings);
|
||||
|
||||
// Add a field with multiple properties.
|
||||
$this->makeField('text', 'field_test_text', 'entity_test_with_bundle', ['bundle1', 'bundle2']);
|
||||
|
||||
// Add two fields that have different internal names but have the same
|
||||
// public name.
|
||||
$this->makeField('entity_reference', 'field_test_alias_a', 'entity_test_with_bundle', ['bundle2'], $settings);
|
||||
$this->makeField('entity_reference', 'field_test_alias_b', 'entity_test_with_bundle', ['bundle3'], $settings);
|
||||
|
||||
$this->resourceTypeRepository = $this->container->get('jsonapi.resource_type.repository');
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::resolveInternalEntityQueryPath
|
||||
* @dataProvider resolveInternalIncludePathProvider
|
||||
*/
|
||||
public function testResolveInternalIncludePath($expect, $external_path, $entity_type_id = 'entity_test_with_bundle', $bundle = 'bundle1'): void {
|
||||
$path_parts = explode('.', $external_path);
|
||||
$resource_type = $this->resourceTypeRepository->get($entity_type_id, $bundle);
|
||||
$this->assertEquals($expect, $this->sut->resolveInternalIncludePath($resource_type, $path_parts));
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides test cases for resolveInternalEntityQueryPath.
|
||||
*/
|
||||
public static function resolveInternalIncludePathProvider() {
|
||||
return [
|
||||
'entity reference' => [[['field_test_ref2']], 'field_test_ref2'],
|
||||
'entity reference with multi target bundles' => [[['field_test_ref1']], 'field_test_ref1'],
|
||||
'entity reference then another entity reference' => [
|
||||
[['field_test_ref1', 'field_test_ref3']],
|
||||
'field_test_ref1.field_test_ref3',
|
||||
],
|
||||
'entity reference with multiple target bundles, each with different field, but the same public field name' => [
|
||||
[
|
||||
['field_test_ref1', 'field_test_alias_a'],
|
||||
['field_test_ref1', 'field_test_alias_b'],
|
||||
],
|
||||
'field_test_ref1.field_test_alias',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Expects an error when an invalid field is provided for include.
|
||||
*
|
||||
* @param string $entity_type
|
||||
* The entity type for which to test field resolution.
|
||||
* @param string $bundle
|
||||
* The entity bundle for which to test field resolution.
|
||||
* @param string $external_path
|
||||
* The external field path to resolve.
|
||||
* @param string $expected_message
|
||||
* (optional) An expected exception message.
|
||||
*
|
||||
* @covers ::resolveInternalIncludePath
|
||||
* @dataProvider resolveInternalIncludePathErrorProvider
|
||||
*/
|
||||
public function testResolveInternalIncludePathError($entity_type, $bundle, $external_path, $expected_message = ''): void {
|
||||
$path_parts = explode('.', $external_path);
|
||||
$this->expectException(CacheableBadRequestHttpException::class);
|
||||
if (!empty($expected_message)) {
|
||||
$this->expectExceptionMessage($expected_message);
|
||||
}
|
||||
$resource_type = $this->resourceTypeRepository->get($entity_type, $bundle);
|
||||
$this->sut->resolveInternalIncludePath($resource_type, $path_parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides test cases for ::testResolveInternalIncludePathError.
|
||||
*/
|
||||
public static function resolveInternalIncludePathErrorProvider() {
|
||||
return [
|
||||
// Should fail because none of these bundles have these fields.
|
||||
['entity_test_with_bundle', 'bundle1', 'host.fail!!.deep'],
|
||||
['entity_test_with_bundle', 'bundle2', 'field_test_ref2'],
|
||||
['entity_test_with_bundle', 'bundle1', 'field_test_ref3'],
|
||||
// Should fail because the nested fields don't exist on the targeted
|
||||
// resource types.
|
||||
['entity_test_with_bundle', 'bundle1', 'field_test_ref1.field_test1'],
|
||||
['entity_test_with_bundle', 'bundle1', 'field_test_ref1.field_test2'],
|
||||
['entity_test_with_bundle', 'bundle1', 'field_test_ref1.field_test_ref1'],
|
||||
['entity_test_with_bundle', 'bundle1', 'field_test_ref1.field_test_ref2'],
|
||||
// Should fail because the nested fields is not a valid relationship
|
||||
// field name.
|
||||
[
|
||||
'entity_test_with_bundle', 'bundle1', 'field_test1',
|
||||
'`field_test1` is not a valid relationship field name.',
|
||||
],
|
||||
// Should fail because the nested fields is not a valid include path.
|
||||
[
|
||||
'entity_test_with_bundle', 'bundle1', 'field_test_ref1.field_test3',
|
||||
'`field_test_ref1.field_test3` is not a valid include path.',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::resolveInternalEntityQueryPath
|
||||
* @dataProvider resolveInternalEntityQueryPathProvider
|
||||
*/
|
||||
public function testResolveInternalEntityQueryPath($expect, $external_path, $entity_type_id = 'entity_test_with_bundle', $bundle = 'bundle1'): void {
|
||||
$resource_type = $this->resourceTypeRepository->get($entity_type_id, $bundle);
|
||||
$this->assertEquals($expect, $this->sut->resolveInternalEntityQueryPath($resource_type, $external_path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides test cases for ::testResolveInternalEntityQueryPath.
|
||||
*/
|
||||
public static function resolveInternalEntityQueryPathProvider() {
|
||||
return [
|
||||
'config entity as base' => [
|
||||
'uuid', 'id', 'entity_test_bundle', 'entity_test_bundle',
|
||||
],
|
||||
'config entity as target' => ['type.entity:entity_test_bundle.uuid', 'type.id'],
|
||||
|
||||
'primitive field; variation A' => ['field_test1', 'field_test1'],
|
||||
'primitive field; variation B' => ['field_test2', 'field_test2'],
|
||||
|
||||
'entity reference then a primitive field; variation A' => [
|
||||
'field_test_ref2.entity:entity_test_with_bundle.field_test1',
|
||||
'field_test_ref2.field_test1',
|
||||
],
|
||||
'entity reference then a primitive field; variation B' => [
|
||||
'field_test_ref2.entity:entity_test_with_bundle.field_test2',
|
||||
'field_test_ref2.field_test2',
|
||||
],
|
||||
|
||||
'entity reference then a complex field with property specifier `value`' => [
|
||||
'field_test_ref2.entity:entity_test_with_bundle.field_test_text.value',
|
||||
'field_test_ref2.field_test_text.value',
|
||||
],
|
||||
'entity reference then a complex field with property specifier `format`' => [
|
||||
'field_test_ref2.entity:entity_test_with_bundle.field_test_text.format',
|
||||
'field_test_ref2.field_test_text.format',
|
||||
],
|
||||
|
||||
'entity reference then no delta with property specifier `id`' => [
|
||||
'field_test_ref1.entity:entity_test_with_bundle.uuid',
|
||||
'field_test_ref1.id',
|
||||
],
|
||||
'entity reference then delta 0 with property specifier `id`' => [
|
||||
'field_test_ref1.0.entity:entity_test_with_bundle.uuid',
|
||||
'field_test_ref1.0.id',
|
||||
],
|
||||
'entity reference then delta 1 with property specifier `id`' => [
|
||||
'field_test_ref1.1.entity:entity_test_with_bundle.uuid',
|
||||
'field_test_ref1.1.id',
|
||||
],
|
||||
|
||||
'entity reference then no reference property and a complex field with property specifier `value`' => [
|
||||
'field_test_ref1.entity:entity_test_with_bundle.field_test_text.value',
|
||||
'field_test_ref1.field_test_text.value',
|
||||
],
|
||||
'entity reference then a reference property and a complex field with property specifier `value`' => [
|
||||
'field_test_ref1.entity.field_test_text.value',
|
||||
'field_test_ref1.entity.field_test_text.value',
|
||||
],
|
||||
'entity reference then no reference property and a complex field with property specifier `format`' => [
|
||||
'field_test_ref1.entity:entity_test_with_bundle.field_test_text.format',
|
||||
'field_test_ref1.field_test_text.format',
|
||||
],
|
||||
'entity reference then a reference property and a complex field with property specifier `format`' => [
|
||||
'field_test_ref1.entity.field_test_text.format',
|
||||
'field_test_ref1.entity.field_test_text.format',
|
||||
],
|
||||
|
||||
'entity reference then property specifier `entity:entity_test_with_bundle` then a complex field with property specifier `value`' => [
|
||||
'field_test_ref1.entity:entity_test_with_bundle.field_test_text.value',
|
||||
'field_test_ref1.entity:entity_test_with_bundle.field_test_text.value',
|
||||
],
|
||||
|
||||
'entity reference with a delta and no reference property then a complex field and property specifier `value`' => [
|
||||
'field_test_ref1.0.entity:entity_test_with_bundle.field_test_text.value',
|
||||
'field_test_ref1.0.field_test_text.value',
|
||||
],
|
||||
'entity reference with a delta and a reference property then a complex field and property specifier `value`' => [
|
||||
'field_test_ref1.0.entity.field_test_text.value',
|
||||
'field_test_ref1.0.entity.field_test_text.value',
|
||||
],
|
||||
|
||||
'entity reference with no reference property then another entity reference with no reference property a complex field with property specifier `value`' => [
|
||||
'field_test_ref1.entity:entity_test_with_bundle.field_test_ref3.entity:entity_test_with_bundle.field_test_text.value',
|
||||
'field_test_ref1.field_test_ref3.field_test_text.value',
|
||||
],
|
||||
'entity reference with a reference property then another entity reference with no reference property a complex field with property specifier `value`' => [
|
||||
'field_test_ref1.entity.field_test_ref3.entity:entity_test_with_bundle.field_test_text.value',
|
||||
'field_test_ref1.entity.field_test_ref3.field_test_text.value',
|
||||
],
|
||||
'entity reference with no reference property then another entity reference with a reference property a complex field with property specifier `value`' => [
|
||||
'field_test_ref1.entity:entity_test_with_bundle.field_test_ref3.entity.field_test_text.value',
|
||||
'field_test_ref1.field_test_ref3.entity.field_test_text.value',
|
||||
],
|
||||
'entity reference with a reference property then another entity reference with a reference property a complex field with property specifier `value`' => [
|
||||
'field_test_ref1.entity.field_test_ref3.entity.field_test_text.value',
|
||||
'field_test_ref1.entity.field_test_ref3.entity.field_test_text.value',
|
||||
],
|
||||
|
||||
'entity reference with target bundles then property specifier `entity:entity_test_with_bundle` then a primitive field on multiple bundles' => [
|
||||
'field_test_ref1.entity:entity_test_with_bundle.field_test3',
|
||||
'field_test_ref1.entity:entity_test_with_bundle.field_test3',
|
||||
],
|
||||
'entity reference without target bundles then property specifier `entity:entity_test_with_bundle` then a primitive field on a single bundle' => [
|
||||
'field_test_ref2.entity:entity_test_with_bundle.field_test1',
|
||||
'field_test_ref2.entity:entity_test_with_bundle.field_test1',
|
||||
],
|
||||
'entity reference without target bundles then property specifier `entity:entity_test_with_bundle` then a primitive field on multiple bundles' => [
|
||||
'field_test_ref3.entity:entity_test_with_bundle.field_test3',
|
||||
'field_test_ref3.entity:entity_test_with_bundle.field_test3',
|
||||
'entity_test_with_bundle', 'bundle2',
|
||||
],
|
||||
'entity reference without target bundles then property specifier `entity:entity_test_with_bundle` then a primitive field on a single bundle starting from a different resource type' => [
|
||||
'field_test_ref3.entity:entity_test_with_bundle.field_test2',
|
||||
'field_test_ref3.entity:entity_test_with_bundle.field_test2',
|
||||
'entity_test_with_bundle', 'bundle3',
|
||||
],
|
||||
|
||||
'entity reference then property specifier `entity:entity_test_with_bundle` then another entity reference before a primitive field' => [
|
||||
'field_test_ref1.entity:entity_test_with_bundle.field_test_ref3.entity:entity_test_with_bundle.field_test2',
|
||||
'field_test_ref1.entity:entity_test_with_bundle.field_test_ref3.field_test2',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Expects an error when an invalid field is provided for filter and sort.
|
||||
*
|
||||
* @param string $entity_type
|
||||
* The entity type for which to test field resolution.
|
||||
* @param string $bundle
|
||||
* The entity bundle for which to test field resolution.
|
||||
* @param string $external_path
|
||||
* The external field path to resolve.
|
||||
* @param string $expected_message
|
||||
* (optional) An expected exception message.
|
||||
*
|
||||
* @covers ::resolveInternalEntityQueryPath
|
||||
* @dataProvider resolveInternalEntityQueryPathErrorProvider
|
||||
*/
|
||||
public function testResolveInternalEntityQueryPathError($entity_type, $bundle, $external_path, $expected_message = ''): void {
|
||||
$this->expectException(CacheableBadRequestHttpException::class);
|
||||
if (!empty($expected_message)) {
|
||||
$this->expectExceptionMessage($expected_message);
|
||||
}
|
||||
$resource_type = $this->resourceTypeRepository->get($entity_type, $bundle);
|
||||
$this->sut->resolveInternalEntityQueryPath($resource_type, $external_path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides test cases for ::testResolveInternalEntityQueryPathError.
|
||||
*/
|
||||
public static function resolveInternalEntityQueryPathErrorProvider() {
|
||||
return [
|
||||
'nested fields' => [
|
||||
'entity_test_with_bundle', 'bundle1', 'none.of.these.exist',
|
||||
],
|
||||
'field does not exist on bundle' => [
|
||||
'entity_test_with_bundle', 'bundle2', 'field_test_ref2',
|
||||
],
|
||||
'field does not exist on different bundle' => [
|
||||
'entity_test_with_bundle', 'bundle1', 'field_test_ref3',
|
||||
],
|
||||
'field does not exist on targeted bundle' => [
|
||||
'entity_test_with_bundle', 'bundle1', 'field_test_ref1.field_test1',
|
||||
],
|
||||
'different field does not exist on same targeted bundle' => [
|
||||
'entity_test_with_bundle', 'bundle1', 'field_test_ref1.field_test2',
|
||||
],
|
||||
'entity reference field does not exist on targeted bundle' => [
|
||||
'entity_test_with_bundle', 'bundle1', 'field_test_ref1.field_test_ref1',
|
||||
],
|
||||
'different entity reference field does not exist on same targeted bundle' => [
|
||||
'entity_test_with_bundle', 'bundle1', 'field_test_ref1.field_test_ref2',
|
||||
],
|
||||
'message correctly identifies missing field' => [
|
||||
'entity_test_with_bundle', 'bundle1',
|
||||
'field_test_ref1.entity:entity_test_with_bundle.field_test1',
|
||||
'Invalid nested filtering. The field `field_test1`, given in the path `field_test_ref1.entity:entity_test_with_bundle.field_test1`, does not exist.',
|
||||
],
|
||||
'message correctly identifies different missing field' => [
|
||||
'entity_test_with_bundle', 'bundle1',
|
||||
'field_test_ref1.entity:entity_test_with_bundle.field_test2',
|
||||
'Invalid nested filtering. The field `field_test2`, given in the path `field_test_ref1.entity:entity_test_with_bundle.field_test2`, does not exist.',
|
||||
],
|
||||
'message correctly identifies missing entity reference field' => [
|
||||
'entity_test_with_bundle', 'bundle2',
|
||||
'field_test_ref1.entity:entity_test_with_bundle.field_test2',
|
||||
'Invalid nested filtering. The field `field_test_ref1`, given in the path `field_test_ref1.entity:entity_test_with_bundle.field_test2`, does not exist.',
|
||||
],
|
||||
|
||||
'entity reference then a complex field with no property specifier' => [
|
||||
'entity_test_with_bundle', 'bundle1',
|
||||
'field_test_ref2.field_test_text',
|
||||
'Invalid nested filtering. The field `field_test_text`, given in the path `field_test_ref2.field_test_text` is incomplete, it must end with one of the following specifiers: `value`, `format`, `processed`.',
|
||||
],
|
||||
|
||||
'entity reference then no delta with property specifier `target_id`' => [
|
||||
'entity_test_with_bundle', 'bundle1',
|
||||
'field_test_ref1.target_id',
|
||||
'Invalid nested filtering. The field `target_id`, given in the path `field_test_ref1.target_id`, does not exist.',
|
||||
],
|
||||
'entity reference then delta 0 with property specifier `target_id`' => [
|
||||
'entity_test_with_bundle', 'bundle1',
|
||||
'field_test_ref1.0.target_id',
|
||||
'Invalid nested filtering. The field `target_id`, given in the path `field_test_ref1.0.target_id`, does not exist.',
|
||||
],
|
||||
'entity reference then delta 1 with property specifier `target_id`' => [
|
||||
'entity_test_with_bundle', 'bundle1',
|
||||
'field_test_ref1.1.target_id',
|
||||
'Invalid nested filtering. The field `target_id`, given in the path `field_test_ref1.1.target_id`, does not exist.',
|
||||
],
|
||||
|
||||
'entity reference then no reference property then a complex field' => [
|
||||
'entity_test_with_bundle', 'bundle1',
|
||||
'field_test_ref1.field_test_text',
|
||||
'Invalid nested filtering. The field `field_test_text`, given in the path `field_test_ref1.field_test_text` is incomplete, it must end with one of the following specifiers: `value`, `format`, `processed`.',
|
||||
|
||||
],
|
||||
'entity reference then reference property then a complex field' => [
|
||||
'entity_test_with_bundle', 'bundle1',
|
||||
'field_test_ref1.entity.field_test_text',
|
||||
'Invalid nested filtering. The field `field_test_text`, given in the path `field_test_ref1.entity.field_test_text` is incomplete, it must end with one of the following specifiers: `value`, `format`, `processed`.',
|
||||
],
|
||||
|
||||
'entity reference then property specifier `entity:entity_test_with_bundle` then a complex field' => [
|
||||
'entity_test_with_bundle', 'bundle1',
|
||||
'field_test_ref1.entity:entity_test_with_bundle.field_test_text',
|
||||
'Invalid nested filtering. The field `field_test_text`, given in the path `field_test_ref1.entity:entity_test_with_bundle.field_test_text` is incomplete, it must end with one of the following specifiers: `value`, `format`, `processed`.',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a simple bundle.
|
||||
*
|
||||
* @param string $name
|
||||
* The name of the bundle to create.
|
||||
*/
|
||||
protected function makeBundle($name): void {
|
||||
EntityTestBundle::create([
|
||||
'id' => $name,
|
||||
])->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a field for a specified entity type/bundle.
|
||||
*
|
||||
* @param string $type
|
||||
* The field type.
|
||||
* @param string $name
|
||||
* The name of the field to create.
|
||||
* @param string $entity_type
|
||||
* The entity type to which the field will be attached.
|
||||
* @param string[] $bundles
|
||||
* The entity bundles to which the field will be attached.
|
||||
* @param array $storage_settings
|
||||
* Custom storage settings for the field.
|
||||
* @param array $config_settings
|
||||
* Custom configuration settings for the field.
|
||||
*/
|
||||
protected function makeField($type, $name, $entity_type, array $bundles, array $storage_settings = [], array $config_settings = []): void {
|
||||
$storage_config = [
|
||||
'field_name' => $name,
|
||||
'type' => $type,
|
||||
'entity_type' => $entity_type,
|
||||
'settings' => $storage_settings,
|
||||
];
|
||||
|
||||
FieldStorageConfig::create($storage_config)->save();
|
||||
|
||||
foreach ($bundles as $bundle) {
|
||||
FieldConfig::create([
|
||||
'field_name' => $name,
|
||||
'entity_type' => $entity_type,
|
||||
'bundle' => $bundle,
|
||||
'settings' => $config_settings,
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,243 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Kernel\Controller;
|
||||
|
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
||||
use Drupal\jsonapi\CacheableResourceResponse;
|
||||
use Drupal\jsonapi\ResourceType\ResourceType;
|
||||
use Drupal\jsonapi\JsonApiResource\Data;
|
||||
use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\Tests\jsonapi\Kernel\JsonapiKernelTestBase;
|
||||
use Drupal\user\Entity\Role;
|
||||
use Drupal\user\Entity\User;
|
||||
use Drupal\user\RoleInterface;
|
||||
use Symfony\Component\HttpFoundation\InputBag;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\jsonapi\Controller\EntityResource
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class EntityResourceTest extends JsonapiKernelTestBase {
|
||||
|
||||
/**
|
||||
* Static UUIDs to use in testing.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $nodeUuid = [
|
||||
1 => '83bc47ad-2c58-45e3-9136-abcdef111111',
|
||||
2 => '83bc47ad-2c58-45e3-9136-abcdef222222',
|
||||
3 => '83bc47ad-2c58-45e3-9136-abcdef333333',
|
||||
4 => '83bc47ad-2c58-45e3-9136-abcdef444444',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'node',
|
||||
'field',
|
||||
'jsonapi',
|
||||
'serialization',
|
||||
'system',
|
||||
'user',
|
||||
];
|
||||
|
||||
/**
|
||||
* The user.
|
||||
*
|
||||
* @var \Drupal\user\Entity\User
|
||||
*/
|
||||
protected $user;
|
||||
|
||||
/**
|
||||
* The node.
|
||||
*
|
||||
* @var \Drupal\node\Entity\Node
|
||||
*/
|
||||
protected $node;
|
||||
|
||||
/**
|
||||
* The other node.
|
||||
*
|
||||
* @var \Drupal\node\Entity\Node
|
||||
*/
|
||||
protected $node2;
|
||||
|
||||
/**
|
||||
* An unpublished node.
|
||||
*
|
||||
* @var \Drupal\node\Entity\Node
|
||||
*/
|
||||
protected $node3;
|
||||
|
||||
/**
|
||||
* A node with related nodes.
|
||||
*
|
||||
* @var \Drupal\node\Entity\Node
|
||||
*/
|
||||
protected Node $node4;
|
||||
|
||||
/**
|
||||
* A fake request.
|
||||
*
|
||||
* @var \Symfony\Component\HttpFoundation\Request
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* The EntityResource under test.
|
||||
*
|
||||
* @var \Drupal\jsonapi\Controller\EntityResource
|
||||
*/
|
||||
protected $entityResource;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
// Add the entity schemas.
|
||||
$this->installEntitySchema('node');
|
||||
$this->installEntitySchema('user');
|
||||
// Add the additional table schemas.
|
||||
$this->installSchema('node', ['node_access']);
|
||||
$this->installSchema('user', ['users_data']);
|
||||
NodeType::create([
|
||||
'type' => 'lorem',
|
||||
'name' => 'Lorem',
|
||||
])->save();
|
||||
$type = NodeType::create([
|
||||
'type' => 'article',
|
||||
'name' => 'Article',
|
||||
]);
|
||||
$type->save();
|
||||
$this->user = User::create([
|
||||
'name' => 'user1',
|
||||
'mail' => 'user@localhost',
|
||||
'status' => 1,
|
||||
'roles' => ['test_role_one', 'test_role_two'],
|
||||
]);
|
||||
$this->createEntityReferenceField('node', 'article', 'field_relationships', 'Relationship', 'node', 'default', ['target_bundles' => ['article']], FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
|
||||
$this->user->save();
|
||||
|
||||
$this->node = Node::create([
|
||||
'title' => 'dummy_title',
|
||||
'type' => 'article',
|
||||
'uid' => $this->user->id(),
|
||||
'uuid' => static::$nodeUuid[1],
|
||||
]);
|
||||
$this->node->save();
|
||||
|
||||
$this->node2 = Node::create([
|
||||
'type' => 'article',
|
||||
'title' => 'Another test node',
|
||||
'uid' => $this->user->id(),
|
||||
'uuid' => static::$nodeUuid[2],
|
||||
]);
|
||||
$this->node2->save();
|
||||
|
||||
$this->node3 = Node::create([
|
||||
'type' => 'article',
|
||||
'title' => 'Unpublished test node',
|
||||
'uid' => $this->user->id(),
|
||||
'status' => 0,
|
||||
'uuid' => static::$nodeUuid[3],
|
||||
]);
|
||||
$this->node3->save();
|
||||
|
||||
$this->node4 = Node::create([
|
||||
'type' => 'article',
|
||||
'title' => 'Test node with related nodes',
|
||||
'uid' => $this->user->id(),
|
||||
'field_relationships' => [
|
||||
['target_id' => $this->node->id()],
|
||||
['target_id' => $this->node2->id()],
|
||||
['target_id' => $this->node3->id()],
|
||||
],
|
||||
'uuid' => static::$nodeUuid[4],
|
||||
]);
|
||||
$this->node4->save();
|
||||
|
||||
// Give anonymous users permission to view user profiles, so that we can
|
||||
// verify the cache tags of cached versions of user profile pages.
|
||||
array_map(function ($role_id) {
|
||||
Role::create([
|
||||
'id' => $role_id,
|
||||
'permissions' => [
|
||||
'access user profiles',
|
||||
'access content',
|
||||
],
|
||||
'label' => $role_id,
|
||||
])->save();
|
||||
}, [RoleInterface::ANONYMOUS_ID, 'test_role_one', 'test_role_two']);
|
||||
|
||||
$this->entityResource = $this->createEntityResource();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of the subject under test.
|
||||
*
|
||||
* @return \Drupal\jsonapi\Controller\EntityResource
|
||||
* An EntityResource instance.
|
||||
*/
|
||||
protected function createEntityResource() {
|
||||
return $this->container->get('jsonapi.entity_resource');
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getCollection
|
||||
*/
|
||||
public function testGetPagedCollection(): void {
|
||||
$request = Request::create('/jsonapi/node/article');
|
||||
$request->query = new InputBag([
|
||||
'sort' => 'nid',
|
||||
'page' => [
|
||||
'offset' => 1,
|
||||
'limit' => 1,
|
||||
],
|
||||
]);
|
||||
|
||||
$entity_resource = $this->createEntityResource();
|
||||
|
||||
// Get the response.
|
||||
$resource_type = $this->container->get('jsonapi.resource_type.repository')->get('node', 'article');
|
||||
$response = $entity_resource->getCollection($resource_type, $request);
|
||||
|
||||
// Assertions.
|
||||
$this->assertInstanceOf(CacheableResourceResponse::class, $response);
|
||||
$this->assertInstanceOf(JsonApiDocumentTopLevel::class, $response->getResponseData());
|
||||
$this->assertInstanceOf(Data::class, $response->getResponseData()->getData());
|
||||
$data = $response->getResponseData()->getData();
|
||||
$this->assertCount(1, $data);
|
||||
$this->assertEquals($this->node2->uuid(), $data->toArray()[0]->getId());
|
||||
$this->assertEqualsCanonicalizing(['node:2', 'node_list'], $response->getCacheableMetadata()->getCacheTags());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getCollection
|
||||
*/
|
||||
public function testGetEmptyCollection(): void {
|
||||
$request = Request::create('/jsonapi/node/article');
|
||||
$request->query = new InputBag(['filter' => ['id' => 'invalid']]);
|
||||
|
||||
// Get the response.
|
||||
$resource_type = new ResourceType('node', 'article', NULL);
|
||||
$response = $this->entityResource->getCollection($resource_type, $request);
|
||||
|
||||
// Assertions.
|
||||
$this->assertInstanceOf(CacheableResourceResponse::class, $response);
|
||||
$this->assertInstanceOf(JsonApiDocumentTopLevel::class, $response->getResponseData());
|
||||
$this->assertInstanceOf(Data::class, $response->getResponseData()->getData());
|
||||
$this->assertEquals(0, $response->getResponseData()->getData()->count());
|
||||
$this->assertEquals(['node_list'], $response->getCacheableMetadata()->getCacheTags());
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Kernel\Controller;
|
||||
|
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
||||
use Drupal\jsonapi\Controller\FileUpload;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\Tests\jsonapi\Kernel\JsonapiKernelTestBase;
|
||||
use Drupal\user\Entity\Role;
|
||||
use Drupal\user\Entity\User;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\jsonapi\Controller\FileUpload
|
||||
* @group jsonapi
|
||||
*/
|
||||
class FileUploadTest extends JsonapiKernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'node',
|
||||
'field',
|
||||
'jsonapi',
|
||||
'serialization',
|
||||
'system',
|
||||
'user',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
// Add the entity schemas.
|
||||
$this->installEntitySchema('node');
|
||||
$this->installEntitySchema('user');
|
||||
// Add the additional table schemas.
|
||||
$this->installSchema('node', ['node_access']);
|
||||
$this->installSchema('user', ['users_data']);
|
||||
NodeType::create([
|
||||
'type' => 'lorem',
|
||||
'name' => 'Lorem',
|
||||
])->save();
|
||||
$type = NodeType::create([
|
||||
'type' => 'article',
|
||||
'name' => 'Article',
|
||||
]);
|
||||
$type->save();
|
||||
$type = NodeType::create([
|
||||
'type' => 'page',
|
||||
'name' => 'Page',
|
||||
]);
|
||||
$type->save();
|
||||
$this->createEntityReferenceField('node', 'article', 'field_relationships', 'Relationship', 'node', 'default', ['target_bundles' => ['article']], FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
|
||||
|
||||
Role::create([
|
||||
'id' => 'article_editor',
|
||||
'label' => 'article editor',
|
||||
'permissions' => [
|
||||
'access content',
|
||||
'create article content',
|
||||
'edit any article content',
|
||||
],
|
||||
])->save();
|
||||
|
||||
Role::create([
|
||||
'id' => 'page_editor',
|
||||
'label' => 'page editor',
|
||||
'permissions' => [
|
||||
'access content',
|
||||
'create page content',
|
||||
'edit any page content',
|
||||
],
|
||||
])->save();
|
||||
|
||||
Role::create([
|
||||
'id' => 'editor',
|
||||
'label' => 'editor',
|
||||
'permissions' => [
|
||||
'bypass node access',
|
||||
],
|
||||
])->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::checkFileUploadAccess
|
||||
*/
|
||||
public function testCheckFileUploadAccessWithBaseField(): void {
|
||||
// Create a set of users for access testing.
|
||||
$article_editor = User::create([
|
||||
'name' => 'article editor',
|
||||
'mail' => 'article@localhost',
|
||||
'status' => 1,
|
||||
// Do not use UID 1 as that has access to everything.
|
||||
'uid' => 2,
|
||||
'roles' => ['article_editor'],
|
||||
]);
|
||||
$page_editor = User::create([
|
||||
'name' => 'page editor',
|
||||
'mail' => 'page@localhost',
|
||||
'status' => 1,
|
||||
'uid' => 3,
|
||||
'roles' => ['page_editor'],
|
||||
]);
|
||||
$editor = User::create([
|
||||
'name' => 'editor',
|
||||
'mail' => 'editor@localhost',
|
||||
'status' => 1,
|
||||
'uid' => 3,
|
||||
'roles' => ['editor'],
|
||||
]);
|
||||
$no_access_user = User::create([
|
||||
'name' => 'no access',
|
||||
'mail' => 'user@localhost',
|
||||
'status' => 1,
|
||||
'uid' => 4,
|
||||
]);
|
||||
|
||||
// Create an entity to test access against.
|
||||
$node = Node::create([
|
||||
'title' => 'dummy_title',
|
||||
'type' => 'article',
|
||||
'uid' => 1,
|
||||
]);
|
||||
|
||||
// While the method is only used to check file fields it should work without
|
||||
// error for any field whether it is a base field or a bundle field.
|
||||
$base_field_definition = $this->container->get('entity_field.manager')->getBaseFieldDefinitions('node')['title'];
|
||||
$bundle_field_definition = $this->container->get('entity_field.manager')->getFieldDefinitions('node', 'article')['field_relationships'];
|
||||
|
||||
// Tests the expected access result for each user.
|
||||
// The $article_editor account can edit any article.
|
||||
$result = FileUpload::checkFileUploadAccess($article_editor, $base_field_definition, $node);
|
||||
$this->assertTrue($result->isAllowed());
|
||||
// The article editor cannot create a node of undetermined type.
|
||||
$result = FileUpload::checkFileUploadAccess($article_editor, $base_field_definition);
|
||||
$this->assertFalse($result->isAllowed());
|
||||
// The article editor can edit any article.
|
||||
$result = FileUpload::checkFileUploadAccess($article_editor, $bundle_field_definition, $node);
|
||||
$this->assertTrue($result->isAllowed());
|
||||
// The article editor can create an article. The type can be determined
|
||||
// because the field is a bundle field.
|
||||
$result = FileUpload::checkFileUploadAccess($article_editor, $bundle_field_definition);
|
||||
$this->assertTrue($result->isAllowed());
|
||||
|
||||
// The $editor account has the bypass node access permissions and can edit
|
||||
// and create all node types.
|
||||
$result = FileUpload::checkFileUploadAccess($editor, $base_field_definition, $node);
|
||||
$this->assertTrue($result->isAllowed());
|
||||
$result = FileUpload::checkFileUploadAccess($editor, $base_field_definition);
|
||||
$this->assertTrue($result->isAllowed());
|
||||
$result = FileUpload::checkFileUploadAccess($editor, $bundle_field_definition, $node);
|
||||
$this->assertTrue($result->isAllowed());
|
||||
$result = FileUpload::checkFileUploadAccess($editor, $bundle_field_definition);
|
||||
$this->assertTrue($result->isAllowed());
|
||||
|
||||
// The $page_editor account can only edit and create pages therefore has no
|
||||
// access.
|
||||
$result = FileUpload::checkFileUploadAccess($page_editor, $base_field_definition, $node);
|
||||
$this->assertFalse($result->isAllowed());
|
||||
$result = FileUpload::checkFileUploadAccess($page_editor, $base_field_definition);
|
||||
$this->assertFalse($result->isAllowed());
|
||||
$result = FileUpload::checkFileUploadAccess($page_editor, $bundle_field_definition, $node);
|
||||
$this->assertFalse($result->isAllowed());
|
||||
$result = FileUpload::checkFileUploadAccess($page_editor, $bundle_field_definition);
|
||||
$this->assertFalse($result->isAllowed());
|
||||
|
||||
// The $no_access_user account has no access at all.
|
||||
$result = FileUpload::checkFileUploadAccess($no_access_user, $base_field_definition, $node);
|
||||
$this->assertFalse($result->isAllowed());
|
||||
$result = FileUpload::checkFileUploadAccess($no_access_user, $base_field_definition);
|
||||
$this->assertFalse($result->isAllowed());
|
||||
$result = FileUpload::checkFileUploadAccess($no_access_user, $bundle_field_definition, $node);
|
||||
$this->assertFalse($result->isAllowed());
|
||||
$result = FileUpload::checkFileUploadAccess($no_access_user, $bundle_field_definition);
|
||||
$this->assertFalse($result->isAllowed());
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Kernel\EventSubscriber;
|
||||
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\entity_test\Entity\EntityTestComputedField;
|
||||
use Drupal\jsonapi\EventSubscriber\ResourceObjectNormalizationCacher;
|
||||
use Drupal\jsonapi\JsonApiResource\ResourceObject;
|
||||
use Drupal\jsonapi\Normalizer\Value\CacheableNormalization;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\user\Entity\User;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Event\TerminateEvent;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\jsonapi\EventSubscriber\ResourceObjectNormalizationCacher
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ResourceObjectNormalizerCacherTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'entity_test',
|
||||
'file',
|
||||
'system',
|
||||
'serialization',
|
||||
'text',
|
||||
'jsonapi',
|
||||
'user',
|
||||
];
|
||||
|
||||
/**
|
||||
* The JSON:API resource type repository.
|
||||
*
|
||||
* @var \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface
|
||||
*/
|
||||
protected $resourceTypeRepository;
|
||||
|
||||
/**
|
||||
* The JSON:API serializer.
|
||||
*
|
||||
* @var \Drupal\jsonapi\Serializer\Serializer
|
||||
*/
|
||||
protected $serializer;
|
||||
|
||||
/**
|
||||
* The object under test.
|
||||
*
|
||||
* @var \Drupal\jsonapi\EventSubscriber\ResourceObjectNormalizationCacher
|
||||
*/
|
||||
protected $cacher;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
// Add the entity schemas.
|
||||
$this->installEntitySchema('user');
|
||||
// Add the additional table schemas.
|
||||
$this->installSchema('user', ['users_data']);
|
||||
$this->resourceTypeRepository = $this->container->get('jsonapi.resource_type.repository');
|
||||
$this->serializer = $this->container->get('jsonapi.serializer');
|
||||
$this->cacher = $this->container->get('jsonapi.normalization_cacher');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that link normalization cache information is not lost.
|
||||
*
|
||||
* @see https://www.drupal.org/project/drupal/issues/3077287
|
||||
*/
|
||||
public function testLinkNormalizationCacheability(): void {
|
||||
$user = User::create([
|
||||
'name' => $this->randomMachineName(),
|
||||
'pass' => $this->randomString(),
|
||||
]);
|
||||
$user->save();
|
||||
$resource_type = $this->resourceTypeRepository->get($user->getEntityTypeId(), $user->bundle());
|
||||
$resource_object = ResourceObject::createFromEntity($resource_type, $user);
|
||||
$cache_tag_to_invalidate = 'link_normalization';
|
||||
$normalized_links = $this->serializer
|
||||
->normalize($resource_object->getLinks(), 'api_json')
|
||||
->withCacheableDependency((new CacheableMetadata())->addCacheTags([$cache_tag_to_invalidate]));
|
||||
assert($normalized_links instanceof CacheableNormalization);
|
||||
$normalization_parts = [
|
||||
ResourceObjectNormalizationCacher::RESOURCE_CACHE_SUBSET_BASE => [
|
||||
'type' => CacheableNormalization::permanent($resource_object->getTypeName()),
|
||||
'id' => CacheableNormalization::permanent($resource_object->getId()),
|
||||
'links' => $normalized_links,
|
||||
],
|
||||
ResourceObjectNormalizationCacher::RESOURCE_CACHE_SUBSET_FIELDS => [],
|
||||
];
|
||||
$this->cacher->saveOnTerminate($resource_object, $normalization_parts);
|
||||
|
||||
$http_kernel = $this->prophesize(HttpKernelInterface::class);
|
||||
$request = $this->prophesize(Request::class);
|
||||
$response = $this->prophesize(Response::class);
|
||||
$event = new TerminateEvent($http_kernel->reveal(), $request->reveal(), $response->reveal());
|
||||
$this->cacher->onTerminate($event);
|
||||
$this->assertNotFalse((bool) $this->cacher->get($resource_object));
|
||||
Cache::invalidateTags([$cache_tag_to_invalidate]);
|
||||
$this->assertFalse((bool) $this->cacher->get($resource_object));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that normalization max-age is correct.
|
||||
*
|
||||
* When max-age for a cached record is set the expiry is set accordingly. But
|
||||
* if the cached normalization is partially used in a later normalization the
|
||||
* max-age should be adjusted to a new timestamp.
|
||||
*
|
||||
* If we don't do this the expires of the cache record will be reset based on
|
||||
* the original max age. This leads to a drift in the expiry time of the
|
||||
* record.
|
||||
*
|
||||
* If a field tells the cache it should expire in exactly 1 hour, then if the
|
||||
* cached data is used 10 minutes later in another resource, that cache should
|
||||
* expire in 50 minutes and not reset to 60 minutes.
|
||||
*/
|
||||
public function testMaxAgeCorrection(): void {
|
||||
$this->installEntitySchema('entity_test_computed_field');
|
||||
|
||||
// Use EntityTestComputedField since ComputedTestCacheableStringItemList has
|
||||
// a max age of 800
|
||||
$baseMaxAge = 800;
|
||||
$entity = EntityTestComputedField::create([]);
|
||||
$entity->save();
|
||||
$resource_type = $this->resourceTypeRepository->get($entity->getEntityTypeId(), $entity->bundle());
|
||||
$resource_object = ResourceObject::createFromEntity($resource_type, $entity);
|
||||
|
||||
$resource_normalization = $this->serializer
|
||||
->normalize($resource_object, 'api_json', ['account' => NULL]);
|
||||
$this->assertEquals($baseMaxAge, $resource_normalization->getCacheMaxAge());
|
||||
|
||||
// Save the normalization to cache, this is done at TerminateEvent.
|
||||
$http_kernel = $this->prophesize(HttpKernelInterface::class);
|
||||
$request = $this->prophesize(Request::class);
|
||||
$response = $this->prophesize(Response::class);
|
||||
$event = new TerminateEvent($http_kernel->reveal(), $request->reveal(), $response->reveal());
|
||||
$this->cacher->onTerminate($event);
|
||||
|
||||
// Change request time to 500 seconds later
|
||||
$current_request = \Drupal::requestStack()->getCurrentRequest();
|
||||
$current_request->server->set('REQUEST_TIME', $current_request->server->get('REQUEST_TIME') + 500);
|
||||
$resource_normalization = $this->serializer
|
||||
->normalize($resource_object, 'api_json', ['account' => NULL]);
|
||||
$this->assertEquals($baseMaxAge - 500, $resource_normalization->getCacheMaxAge(), 'Max age should be 300 since 500 seconds has passed');
|
||||
|
||||
// Change request time to 800 seconds later, this is the last second the
|
||||
// cache backend would return cached data. The max-age at that time should
|
||||
// be 0 which is the same as the expire time of the cache entry.
|
||||
$current_request->server->set('REQUEST_TIME', $current_request->server->get('REQUEST_TIME') + 800);
|
||||
$resource_normalization = $this->serializer
|
||||
->normalize($resource_object, 'api_json', ['account' => NULL]);
|
||||
$this->assertEquals(0, $resource_normalization->getCacheMaxAge(), 'Max age should be 0 since max-age has passed');
|
||||
|
||||
// Change request time to 801 seconds later. This validates that max-age
|
||||
// never becomes negative. This should never happen as the cache entry
|
||||
// is expired at this time and the cache backend would not return data.
|
||||
$current_request->server->set('REQUEST_TIME', $current_request->server->get('REQUEST_TIME') + 801);
|
||||
$resource_normalization = $this->serializer
|
||||
->normalize($resource_object, 'api_json', ['account' => NULL]);
|
||||
$this->assertEquals(0, $resource_normalization->getCacheMaxAge(), 'Max age should be 0 since max-age has passed a second ago');
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Kernel;
|
||||
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
|
||||
|
||||
/**
|
||||
* Contains shared test utility methods.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
abstract class JsonapiKernelTestBase extends KernelTestBase {
|
||||
|
||||
use EntityReferenceFieldCreationTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['jsonapi', 'file'];
|
||||
|
||||
/**
|
||||
* Creates a field of a text field storage on the bundle.
|
||||
*
|
||||
* @param string $entity_type
|
||||
* The type of entity the field will be attached to.
|
||||
* @param string $bundle
|
||||
* The bundle name of the entity the field will be attached to.
|
||||
* @param string $field_name
|
||||
* The name of the field; if it exists, a new instance of the existing.
|
||||
* field will be created.
|
||||
* @param string $field_label
|
||||
* The label of the field.
|
||||
* @param int $cardinality
|
||||
* The cardinality of the field.
|
||||
*
|
||||
* @see \Drupal\Core\Entity\Plugin\EntityReferenceSelection\SelectionBase::buildConfigurationForm()
|
||||
*/
|
||||
protected function createTextField($entity_type, $bundle, $field_name, $field_label, $cardinality = 1) {
|
||||
// Look for or add the specified field to the requested entity bundle.
|
||||
if (!FieldStorageConfig::loadByName($entity_type, $field_name)) {
|
||||
FieldStorageConfig::create([
|
||||
'field_name' => $field_name,
|
||||
'type' => 'text',
|
||||
'entity_type' => $entity_type,
|
||||
'cardinality' => $cardinality,
|
||||
])->save();
|
||||
}
|
||||
if (!FieldConfig::loadByName($entity_type, $bundle, $field_name)) {
|
||||
FieldConfig::create([
|
||||
'field_name' => $field_name,
|
||||
'entity_type' => $entity_type,
|
||||
'bundle' => $bundle,
|
||||
'label' => $field_label,
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Kernel\Normalizer;
|
||||
|
||||
use Drupal\Core\Field\BaseFieldDefinition;
|
||||
use Drupal\Core\Field\FieldItemInterface;
|
||||
use Drupal\entity_test\Entity\EntityTest;
|
||||
use Drupal\jsonapi\Normalizer\FieldItemNormalizer;
|
||||
use Drupal\jsonapi\Normalizer\Value\CacheableNormalization;
|
||||
use Drupal\Tests\jsonapi\Kernel\JsonapiKernelTestBase;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\jsonapi\Normalizer\FieldItemNormalizer
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class FieldItemNormalizerTest extends JsonapiKernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'file',
|
||||
'system',
|
||||
'user',
|
||||
'link',
|
||||
'entity_test',
|
||||
'serialization',
|
||||
];
|
||||
|
||||
/**
|
||||
* The normalizer.
|
||||
*
|
||||
* @var \Drupal\jsonapi\Normalizer\FieldItemNormalizer
|
||||
*/
|
||||
private $normalizer;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$etm = $this->container->get('entity_type.manager');
|
||||
$this->normalizer = new FieldItemNormalizer($etm);
|
||||
$this->normalizer->setSerializer($this->container->get('jsonapi.serializer'));
|
||||
|
||||
$definitions = [];
|
||||
$definitions['links'] = BaseFieldDefinition::create('link')->setLabel('Links');
|
||||
$definitions['internal_property_value'] = BaseFieldDefinition::create('single_internal_property_test')->setLabel('Internal property');
|
||||
$definitions['no_main_property_value'] = BaseFieldDefinition::create('map')->setLabel('No main property');
|
||||
$this->container->get('state')->set('entity_test.additional_base_field_definitions', $definitions);
|
||||
$etm->clearCachedDefinitions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests a field item that has no properties.
|
||||
*
|
||||
* @covers ::normalize
|
||||
*/
|
||||
public function testNormalizeFieldItemWithoutProperties(): void {
|
||||
$item = $this->prophesize(FieldItemInterface::class);
|
||||
$item->getProperties(TRUE)->willReturn([]);
|
||||
$item->getValue()->willReturn('Direct call to getValue');
|
||||
|
||||
$result = $this->normalizer->normalize($item->reveal(), 'api_json');
|
||||
assert($result instanceof CacheableNormalization);
|
||||
$this->assertSame('Direct call to getValue', $result->getNormalization());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests normalizing field item.
|
||||
*/
|
||||
public function testNormalizeFieldItem(): void {
|
||||
$entity = EntityTest::create([
|
||||
'name' => 'Test entity',
|
||||
'links' => [
|
||||
[
|
||||
'uri' => 'https://www.drupal.org',
|
||||
'title' => 'Drupal.org',
|
||||
'options' => [
|
||||
'query' => 'foo=bar',
|
||||
],
|
||||
],
|
||||
],
|
||||
'internal_property_value' => [
|
||||
[
|
||||
'value' => 'Internal property testing!',
|
||||
],
|
||||
],
|
||||
'no_main_property_value' => [
|
||||
[
|
||||
'value' => 'No main property testing!',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// Verify a field with one property is flattened.
|
||||
$result = $this->normalizer->normalize($entity->get('name')->first());
|
||||
assert($result instanceof CacheableNormalization);
|
||||
$this->assertEquals('Test entity', $result->getNormalization());
|
||||
|
||||
// Verify a field with multiple public properties has all of them returned.
|
||||
$result = $this->normalizer->normalize($entity->get('links')->first());
|
||||
assert($result instanceof CacheableNormalization);
|
||||
$this->assertEquals([
|
||||
'uri' => 'https://www.drupal.org',
|
||||
'title' => 'Drupal.org',
|
||||
'options' => [
|
||||
'query' => 'foo=bar',
|
||||
],
|
||||
], $result->getNormalization());
|
||||
|
||||
// Verify a field with one public property and one internal only returns the
|
||||
// public property, and is flattened.
|
||||
$result = $this->normalizer->normalize($entity->get('internal_property_value')->first());
|
||||
assert($result instanceof CacheableNormalization);
|
||||
// Property `internal_value` will not exist.
|
||||
$this->assertEquals('Internal property testing!', $result->getNormalization());
|
||||
|
||||
// Verify a field with one public property but no main property is not
|
||||
// flattened.
|
||||
$result = $this->normalizer->normalize($entity->get('no_main_property_value')->first());
|
||||
assert($result instanceof CacheableNormalization);
|
||||
$this->assertEquals([
|
||||
'value' => 'No main property testing!',
|
||||
], $result->getNormalization());
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,937 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Kernel\Normalizer;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\file\Entity\File;
|
||||
use Drupal\jsonapi\JsonApiResource\ErrorCollection;
|
||||
use Drupal\jsonapi\JsonApiResource\LabelOnlyResourceObject;
|
||||
use Drupal\jsonapi\JsonApiResource\LinkCollection;
|
||||
use Drupal\jsonapi\JsonApiResource\NullIncludedData;
|
||||
use Drupal\jsonapi\JsonApiResource\ResourceObject;
|
||||
use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel;
|
||||
use Drupal\jsonapi\JsonApiResource\ResourceObjectData;
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\jsonapi\Normalizer\JsonApiDocumentTopLevelNormalizer;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\system\Entity\Action;
|
||||
use Drupal\taxonomy\Entity\Term;
|
||||
use Drupal\taxonomy\Entity\Vocabulary;
|
||||
use Drupal\Tests\image\Kernel\ImageFieldCreationTrait;
|
||||
use Drupal\Tests\jsonapi\Kernel\JsonapiKernelTestBase;
|
||||
use Drupal\Tests\jsonapi\Traits\JsonApiJsonSchemaTestTrait;
|
||||
use Drupal\user\Entity\Role;
|
||||
use Drupal\user\Entity\User;
|
||||
use Drupal\user\RoleInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\jsonapi\Normalizer\JsonApiDocumentTopLevelNormalizer
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class JsonApiTopLevelResourceNormalizerTest extends JsonapiKernelTestBase {
|
||||
|
||||
use ImageFieldCreationTrait;
|
||||
use JsonApiJsonSchemaTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'jsonapi',
|
||||
'field',
|
||||
'node',
|
||||
'serialization',
|
||||
'system',
|
||||
'taxonomy',
|
||||
'text',
|
||||
'filter',
|
||||
'user',
|
||||
'file',
|
||||
'image',
|
||||
'jsonapi_test_normalizers_kernel',
|
||||
'jsonapi_test_resource_type_building',
|
||||
];
|
||||
|
||||
/**
|
||||
* A node to normalize.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityInterface
|
||||
*/
|
||||
protected $node;
|
||||
|
||||
/**
|
||||
* The node type.
|
||||
*
|
||||
* @var \Drupal\node\Entity\NodeType
|
||||
*/
|
||||
protected NodeType $nodeType;
|
||||
|
||||
/**
|
||||
* A user to normalize.
|
||||
*
|
||||
* @var \Drupal\user\Entity\User
|
||||
*/
|
||||
protected $user;
|
||||
|
||||
/**
|
||||
* A user.
|
||||
*
|
||||
* @var \Drupal\user\Entity\User
|
||||
*/
|
||||
protected User $user2;
|
||||
|
||||
/**
|
||||
* A vocabulary.
|
||||
*
|
||||
* @var \Drupal\taxonomy\Entity\Vocabulary
|
||||
*/
|
||||
protected Vocabulary $vocabulary;
|
||||
|
||||
/**
|
||||
* A term.
|
||||
*
|
||||
* @var \Drupal\taxonomy\Entity\Term
|
||||
*/
|
||||
protected Term $term1;
|
||||
|
||||
/**
|
||||
* A term.
|
||||
*
|
||||
* @var \Drupal\taxonomy\Entity\Term
|
||||
*/
|
||||
protected Term $term2;
|
||||
|
||||
/**
|
||||
* The include resolver.
|
||||
*
|
||||
* @var \Drupal\jsonapi\IncludeResolver
|
||||
*/
|
||||
protected $includeResolver;
|
||||
|
||||
/**
|
||||
* The JSON:API resource type repository under test.
|
||||
*
|
||||
* @var \Drupal\jsonapi\ResourceType\ResourceTypeRepository
|
||||
*/
|
||||
protected $resourceTypeRepository;
|
||||
|
||||
/**
|
||||
* @var \Drupal\file\Entity\File
|
||||
*/
|
||||
private $file;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
// Add the entity schemas.
|
||||
$this->installEntitySchema('node');
|
||||
$this->installEntitySchema('user');
|
||||
$this->installEntitySchema('taxonomy_term');
|
||||
$this->installEntitySchema('file');
|
||||
// Add the additional table schemas.
|
||||
$this->installSchema('node', ['node_access']);
|
||||
$this->installSchema('user', ['users_data']);
|
||||
$this->installSchema('file', ['file_usage']);
|
||||
$type = NodeType::create([
|
||||
'type' => 'article',
|
||||
'name' => 'Article',
|
||||
]);
|
||||
$type->save();
|
||||
$this->createEntityReferenceField(
|
||||
'node',
|
||||
'article',
|
||||
'field_tags',
|
||||
'Tags',
|
||||
'taxonomy_term',
|
||||
'default',
|
||||
['target_bundles' => ['tags']],
|
||||
FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
|
||||
);
|
||||
$this->createTextField('node', 'article', 'body', 'Body');
|
||||
|
||||
$this->createImageField('field_image', 'node', 'article');
|
||||
|
||||
$this->user = User::create([
|
||||
'name' => 'user1',
|
||||
'mail' => 'user@localhost',
|
||||
]);
|
||||
$this->user2 = User::create([
|
||||
'name' => 'user2',
|
||||
'mail' => 'user2@localhost',
|
||||
]);
|
||||
|
||||
$this->user->save();
|
||||
$this->user2->save();
|
||||
|
||||
$this->vocabulary = Vocabulary::create(['name' => 'Tags', 'vid' => 'tags']);
|
||||
$this->vocabulary->save();
|
||||
|
||||
$this->term1 = Term::create([
|
||||
'name' => 'term1',
|
||||
'vid' => $this->vocabulary->id(),
|
||||
]);
|
||||
$this->term2 = Term::create([
|
||||
'name' => 'term2',
|
||||
'vid' => $this->vocabulary->id(),
|
||||
]);
|
||||
|
||||
$this->term1->save();
|
||||
$this->term2->save();
|
||||
|
||||
$this->file = File::create([
|
||||
'uri' => 'public://example.png',
|
||||
'filename' => 'example.png',
|
||||
]);
|
||||
$this->file->save();
|
||||
|
||||
$this->node = Node::create([
|
||||
'title' => 'dummy_title',
|
||||
'type' => 'article',
|
||||
'uid' => $this->user,
|
||||
'body' => [
|
||||
'format' => 'plain_text',
|
||||
'value' => $this->randomString(),
|
||||
],
|
||||
'field_tags' => [
|
||||
['target_id' => $this->term1->id()],
|
||||
['target_id' => $this->term2->id()],
|
||||
],
|
||||
'field_image' => [
|
||||
[
|
||||
'target_id' => $this->file->id(),
|
||||
'alt' => 'test alt',
|
||||
'title' => 'test title',
|
||||
'width' => 10,
|
||||
'height' => 11,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->node->save();
|
||||
|
||||
$this->nodeType = NodeType::load('article');
|
||||
|
||||
Role::create([
|
||||
'id' => RoleInterface::ANONYMOUS_ID,
|
||||
'permissions' => [
|
||||
'access content',
|
||||
],
|
||||
'label' => 'Anonymous',
|
||||
])->save();
|
||||
|
||||
$this->includeResolver = $this->container->get('jsonapi.include_resolver');
|
||||
$this->resourceTypeRepository = $this->container->get('jsonapi.resource_type.repository');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function tearDown(): void {
|
||||
if ($this->node) {
|
||||
$this->node->delete();
|
||||
}
|
||||
if ($this->term1) {
|
||||
$this->term1->delete();
|
||||
}
|
||||
if ($this->term2) {
|
||||
$this->term2->delete();
|
||||
}
|
||||
if ($this->vocabulary) {
|
||||
$this->vocabulary->delete();
|
||||
}
|
||||
if ($this->user) {
|
||||
$this->user->delete();
|
||||
}
|
||||
if ($this->user2) {
|
||||
$this->user2->delete();
|
||||
}
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a test resource type, resource object and includes.
|
||||
*
|
||||
* @return array
|
||||
* Indexed array with values:
|
||||
* - Resource type.
|
||||
* - Resource object.
|
||||
* - Includes.
|
||||
*/
|
||||
protected function getTestContentEntityResource(): array {
|
||||
$resource_type = $this->container->get('jsonapi.resource_type.repository')
|
||||
->get('node', 'article');
|
||||
|
||||
$resource_object = ResourceObject::createFromEntity($resource_type, $this->node);
|
||||
$includes = $this->includeResolver->resolve($resource_object, 'uid,field_tags,field_image');
|
||||
return [$resource_type, $resource_object, $includes];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a test resource type, resource object and includes for config entity.
|
||||
*
|
||||
* @return array
|
||||
* Indexed array with values:
|
||||
* - Resource type.
|
||||
* - Resource object.
|
||||
* - Includes.
|
||||
*/
|
||||
protected function getTestConfigEntityResource(): array {
|
||||
$resource_type = $this->container->get('jsonapi.resource_type.repository')
|
||||
->get('action', 'action');
|
||||
|
||||
$resource_object = ResourceObject::createFromEntity(
|
||||
$resource_type,
|
||||
Action::create([
|
||||
'id' => 'user_add_role_action.' . RoleInterface::ANONYMOUS_ID,
|
||||
'type' => 'user',
|
||||
'label' => 'Add the anonymous role to the selected users',
|
||||
'configuration' => [
|
||||
'rid' => RoleInterface::ANONYMOUS_ID,
|
||||
],
|
||||
'plugin' => 'user_add_role_action',
|
||||
])
|
||||
);
|
||||
return [$resource_type, $resource_object, new NullIncludedData()];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::normalize
|
||||
*/
|
||||
public function testNormalize(): void {
|
||||
[$resource_type, $resource_object, $includes] = $this->getTestContentEntityResource();
|
||||
$jsonapi_doc_object = $this
|
||||
->getNormalizer()
|
||||
->normalize(
|
||||
new JsonApiDocumentTopLevel(new ResourceObjectData([$resource_object], 1), $includes, new LinkCollection([])),
|
||||
'api_json',
|
||||
[
|
||||
'resource_type' => $resource_type,
|
||||
'account' => NULL,
|
||||
'sparse_fieldset' => [
|
||||
'node--article' => [
|
||||
'title',
|
||||
'node_type',
|
||||
'uid',
|
||||
'field_tags',
|
||||
'field_image',
|
||||
],
|
||||
'user--user' => [
|
||||
'display_name',
|
||||
],
|
||||
],
|
||||
'include' => [
|
||||
'uid',
|
||||
'field_tags',
|
||||
'field_image',
|
||||
],
|
||||
]
|
||||
);
|
||||
$normalized = $jsonapi_doc_object->getNormalization();
|
||||
|
||||
// @see http://jsonapi.org/format/#document-jsonapi-object
|
||||
$this->assertEquals(JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION, $normalized['jsonapi']['version']);
|
||||
$this->assertEquals(JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK, $normalized['jsonapi']['meta']['links']['self']['href']);
|
||||
|
||||
$this->assertSame($normalized['data']['attributes']['title'], 'dummy_title');
|
||||
$this->assertEquals($normalized['data']['id'], $this->node->uuid());
|
||||
$this->assertSame([
|
||||
'data' => [
|
||||
'type' => 'node_type--node_type',
|
||||
'id' => NodeType::load('article')->uuid(),
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => 'article',
|
||||
],
|
||||
],
|
||||
'links' => [
|
||||
'related' => ['href' => Url::fromUri('internal:/jsonapi/node/article/' . $this->node->uuid() . '/node_type', ['query' => ['resourceVersion' => 'id:' . $this->node->getRevisionId()]])->setAbsolute()->toString(TRUE)->getGeneratedUrl()],
|
||||
'self' => ['href' => Url::fromUri('internal:/jsonapi/node/article/' . $this->node->uuid() . '/relationships/node_type', ['query' => ['resourceVersion' => 'id:' . $this->node->getRevisionId()]])->setAbsolute()->toString(TRUE)->getGeneratedUrl()],
|
||||
],
|
||||
], $normalized['data']['relationships']['node_type']);
|
||||
$this->assertTrue(!isset($normalized['data']['attributes']['created']));
|
||||
$this->assertEquals([
|
||||
'alt' => 'test alt',
|
||||
'title' => 'test title',
|
||||
'width' => 10,
|
||||
'height' => 11,
|
||||
'drupal_internal__target_id' => $this->file->id(),
|
||||
], $normalized['data']['relationships']['field_image']['data']['meta']);
|
||||
$this->assertSame('node--article', $normalized['data']['type']);
|
||||
$this->assertEquals([
|
||||
'data' => [
|
||||
'type' => 'user--user',
|
||||
'id' => $this->user->uuid(),
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => $this->user->id(),
|
||||
],
|
||||
],
|
||||
'links' => [
|
||||
'self' => ['href' => Url::fromUri('internal:/jsonapi/node/article/' . $this->node->uuid() . '/relationships/uid', ['query' => ['resourceVersion' => 'id:' . $this->node->getRevisionId()]])->setAbsolute()->toString(TRUE)->getGeneratedUrl()],
|
||||
'related' => ['href' => Url::fromUri('internal:/jsonapi/node/article/' . $this->node->uuid() . '/uid', ['query' => ['resourceVersion' => 'id:' . $this->node->getRevisionId()]])->setAbsolute()->toString(TRUE)->getGeneratedUrl()],
|
||||
],
|
||||
], $normalized['data']['relationships']['uid']);
|
||||
$this->assertArrayNotHasKey('meta', $normalized);
|
||||
$this->assertSame($this->user->uuid(), $normalized['included'][0]['id']);
|
||||
$this->assertSame('user--user', $normalized['included'][0]['type']);
|
||||
$this->assertSame('user1', $normalized['included'][0]['attributes']['display_name']);
|
||||
$this->assertCount(1, $normalized['included'][0]['attributes']);
|
||||
$this->assertSame($this->term1->uuid(), $normalized['included'][1]['id']);
|
||||
$this->assertSame('taxonomy_term--tags', $normalized['included'][1]['type']);
|
||||
$this->assertSame($this->term1->label(), $normalized['included'][1]['attributes']['name']);
|
||||
$this->assertCount(11, $normalized['included'][1]['attributes']);
|
||||
$this->assertTrue(!isset($normalized['included'][1]['attributes']['created']));
|
||||
// Make sure that the cache tags for the includes and the requested entities
|
||||
// are bubbling as expected.
|
||||
$this->assertEqualsCanonicalizing(
|
||||
['file:1', 'node:1', 'taxonomy_term:1', 'taxonomy_term:2', 'user:1'],
|
||||
$jsonapi_doc_object->getCacheTags()
|
||||
);
|
||||
$this->assertSame(
|
||||
Cache::PERMANENT,
|
||||
$jsonapi_doc_object->getCacheMaxAge()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::normalize
|
||||
*/
|
||||
public function testNormalizeUuid(): void {
|
||||
$resource_type = $this->container->get('jsonapi.resource_type.repository')->get('node', 'article');
|
||||
$resource_object = ResourceObject::createFromEntity($resource_type, $this->node);
|
||||
$include_param = 'uid,field_tags';
|
||||
$includes = $this->includeResolver->resolve($resource_object, $include_param);
|
||||
$document_wrapper = new JsonApiDocumentTopLevel(new ResourceObjectData([$resource_object], 1), $includes, new LinkCollection([]));
|
||||
|
||||
$jsonapi_doc_object = $this
|
||||
->getNormalizer()
|
||||
->normalize(
|
||||
$document_wrapper,
|
||||
'api_json',
|
||||
[
|
||||
'resource_type' => $resource_type,
|
||||
'account' => NULL,
|
||||
'include' => [
|
||||
'uid',
|
||||
'field_tags',
|
||||
],
|
||||
]
|
||||
);
|
||||
$normalized = $jsonapi_doc_object->getNormalization();
|
||||
$this->assertStringMatchesFormat($this->node->uuid(), $normalized['data']['id']);
|
||||
$this->assertEquals($this->node->type->entity->uuid(), $normalized['data']['relationships']['node_type']['data']['id']);
|
||||
$this->assertEquals($this->user->uuid(), $normalized['data']['relationships']['uid']['data']['id']);
|
||||
$this->assertNotEmpty($normalized['included'][0]['id']);
|
||||
$this->assertArrayNotHasKey('meta', $normalized);
|
||||
$this->assertEquals($this->user->uuid(), $normalized['included'][0]['id']);
|
||||
$this->assertCount(1, $normalized['included'][0]['attributes']);
|
||||
$this->assertCount(11, $normalized['included'][1]['attributes']);
|
||||
// Make sure that the cache tags for the includes and the requested entities
|
||||
// are bubbling as expected.
|
||||
$this->assertEqualsCanonicalizing(
|
||||
['node:1', 'taxonomy_term:1', 'taxonomy_term:2', 'user:1'],
|
||||
$jsonapi_doc_object->getCacheTags()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::normalize
|
||||
*/
|
||||
public function testNormalizeException(): void {
|
||||
$normalized = $this
|
||||
->container
|
||||
->get('jsonapi.serializer')
|
||||
->normalize(
|
||||
new JsonApiDocumentTopLevel(new ErrorCollection([new BadRequestHttpException('Lorem')]), new NullIncludedData(), new LinkCollection([])),
|
||||
'api_json',
|
||||
[]
|
||||
)->getNormalization();
|
||||
$this->assertNotEmpty($normalized['errors']);
|
||||
$this->assertArrayNotHasKey('data', $normalized);
|
||||
$this->assertEquals(400, $normalized['errors'][0]['status']);
|
||||
$this->assertEquals('Lorem', $normalized['errors'][0]['detail']);
|
||||
$this->assertEquals([
|
||||
'info' => [
|
||||
'href' => 'https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.1',
|
||||
],
|
||||
'via' => ['href' => 'http://localhost/'],
|
||||
], $normalized['errors'][0]['links']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the message and exceptions when requesting a Label only resource.
|
||||
*/
|
||||
public function testAliasFieldRouteException(): void {
|
||||
$this->assertSame('uid', $this->resourceTypeRepository->getByTypeName('node--article')->getPublicName('uid'));
|
||||
$this->assertSame('roles', $this->resourceTypeRepository->getByTypeName('user--user')->getPublicName('roles'));
|
||||
$resource_type_field_aliases = [
|
||||
'node--article' => [
|
||||
'uid' => 'author',
|
||||
],
|
||||
'user--user' => [
|
||||
'roles' => 'user_roles',
|
||||
],
|
||||
];
|
||||
\Drupal::state()->set('jsonapi_test_resource_type_builder.resource_type_field_aliases', $resource_type_field_aliases);
|
||||
Cache::invalidateTags(['jsonapi_resource_types']);
|
||||
$this->assertSame('author', $this->resourceTypeRepository->getByTypeName('node--article')->getPublicName('uid'));
|
||||
$this->assertSame('user_roles', $this->resourceTypeRepository->getByTypeName('user--user')->getPublicName('roles'));
|
||||
|
||||
// Create the request to fetch the articles and fetch included user.
|
||||
$resource_type = $this->container->get('jsonapi.resource_type.repository')->get('node', 'article');
|
||||
$user = User::load($this->node->getOwnerId());
|
||||
|
||||
$resource_object = ResourceObject::createFromEntity($resource_type, $this->node);
|
||||
$user_resource_type = $this->container->get('jsonapi.resource_type.repository')->get('user', 'user');
|
||||
|
||||
$resource_object_user = LabelOnlyResourceObject::createFromEntity($user_resource_type, $user);
|
||||
$includes = $this->includeResolver->resolve($resource_object_user, 'user_roles');
|
||||
|
||||
/** @var \Drupal\jsonapi\Normalizer\Value\CacheableNormalization $jsonapi_doc_object */
|
||||
$jsonapi_doc_object = $this
|
||||
->getNormalizer()
|
||||
->normalize(
|
||||
new JsonApiDocumentTopLevel(new ResourceObjectData([$resource_object, $resource_object_user], 2), $includes, new LinkCollection([])),
|
||||
'api_json',
|
||||
[
|
||||
'resource_type' => $resource_type,
|
||||
'account' => NULL,
|
||||
'sparse_fieldset' => [
|
||||
'node--article' => [
|
||||
'title',
|
||||
'node_type',
|
||||
'uid',
|
||||
],
|
||||
'user--user' => [
|
||||
'user_roles',
|
||||
],
|
||||
],
|
||||
'include' => [
|
||||
'user_roles',
|
||||
],
|
||||
],
|
||||
)->getNormalization();
|
||||
$this->assertNotEmpty($jsonapi_doc_object['meta']['omitted']);
|
||||
foreach ($jsonapi_doc_object['meta']['omitted']['links'] as $key => $link) {
|
||||
if (str_starts_with($key, 'item--')) {
|
||||
// Ensure that resource link contains URL with the alias field.
|
||||
$resource_link = Url::fromUri('internal:/jsonapi/user/user/' . $user->uuid() . '/user_roles')->setAbsolute()->toString(TRUE);
|
||||
$this->assertEquals($resource_link->getGeneratedUrl(), $link['href']);
|
||||
$this->assertEquals("The current user is not allowed to view this relationship. The user only has authorization for the 'view label' operation.", $link['meta']['detail']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::normalize
|
||||
*/
|
||||
public function testNormalizeConfig(): void {
|
||||
$resource_type = $this->container->get('jsonapi.resource_type.repository')->get('node_type', 'node_type');
|
||||
$resource_object = ResourceObject::createFromEntity($resource_type, $this->nodeType);
|
||||
$document_wrapper = new JsonApiDocumentTopLevel(new ResourceObjectData([$resource_object], 1), new NullIncludedData(), new LinkCollection([]));
|
||||
|
||||
$jsonapi_doc_object = $this
|
||||
->getNormalizer()
|
||||
->normalize($document_wrapper, 'api_json', [
|
||||
'resource_type' => $resource_type,
|
||||
'account' => NULL,
|
||||
'sparse_fieldset' => [
|
||||
'node_type--node_type' => [
|
||||
'description',
|
||||
'display_submitted',
|
||||
],
|
||||
],
|
||||
]);
|
||||
$normalized = $jsonapi_doc_object->getNormalization();
|
||||
$this->assertSame(['description', 'display_submitted'], array_keys($normalized['data']['attributes']));
|
||||
$this->assertSame($normalized['data']['id'], NodeType::load('article')->uuid());
|
||||
$this->assertSame($normalized['data']['type'], 'node_type--node_type');
|
||||
// Make sure that the cache tags for the includes and the requested entities
|
||||
// are bubbling as expected.
|
||||
$this->assertSame(['config:node.type.article'], $jsonapi_doc_object->getCacheTags());
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to POST a node and check if it exists afterwards.
|
||||
*
|
||||
* @covers ::denormalize
|
||||
*/
|
||||
public function testDenormalize(): void {
|
||||
$payload = '{"data":{"type":"article","attributes":{"title":"Testing article"}}}';
|
||||
|
||||
$resource_type = $this->container->get('jsonapi.resource_type.repository')->get('node', 'article');
|
||||
$node = $this
|
||||
->getNormalizer()
|
||||
->denormalize(Json::decode($payload), NULL, 'api_json', [
|
||||
'resource_type' => $resource_type,
|
||||
]);
|
||||
$this->assertInstanceOf(Node::class, $node);
|
||||
$this->assertSame('Testing article', $node->getTitle());
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to POST a node and check if it exists afterwards.
|
||||
*
|
||||
* @covers ::denormalize
|
||||
*/
|
||||
public function testDenormalizeUuid(): void {
|
||||
$configurations = [
|
||||
// Good data.
|
||||
[
|
||||
[
|
||||
[$this->term2->uuid(), $this->term1->uuid()],
|
||||
$this->user2->uuid(),
|
||||
],
|
||||
[
|
||||
[$this->term2->id(), $this->term1->id()],
|
||||
$this->user2->id(),
|
||||
],
|
||||
],
|
||||
// Good data, without any tags.
|
||||
[
|
||||
[
|
||||
[],
|
||||
$this->user2->uuid(),
|
||||
],
|
||||
[
|
||||
[],
|
||||
$this->user2->id(),
|
||||
],
|
||||
],
|
||||
// Bad data in first tag.
|
||||
[
|
||||
[
|
||||
['invalid-uuid', $this->term1->uuid()],
|
||||
$this->user2->uuid(),
|
||||
],
|
||||
[
|
||||
[$this->term1->id()],
|
||||
$this->user2->id(),
|
||||
],
|
||||
'taxonomy_term--tags:invalid-uuid',
|
||||
],
|
||||
// Bad data in user and first tag.
|
||||
[
|
||||
[
|
||||
['invalid-uuid', $this->term1->uuid()],
|
||||
'also-invalid-uuid',
|
||||
],
|
||||
[
|
||||
[$this->term1->id()],
|
||||
NULL,
|
||||
],
|
||||
'user--user:also-invalid-uuid',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($configurations as $configuration) {
|
||||
[$payload_data, $expected] = $this->denormalizeUuidProviderBuilder($configuration);
|
||||
$payload = Json::encode($payload_data);
|
||||
|
||||
$resource_type = $this->container->get('jsonapi.resource_type.repository')->get('node', 'article');
|
||||
try {
|
||||
$node = $this
|
||||
->getNormalizer()
|
||||
->denormalize(Json::decode($payload), NULL, 'api_json', [
|
||||
'resource_type' => $resource_type,
|
||||
]);
|
||||
}
|
||||
catch (NotFoundHttpException $e) {
|
||||
$non_existing_resource_identifier = $configuration[2];
|
||||
$this->assertEquals("The resource identified by `$non_existing_resource_identifier` (given as a relationship item) could not be found.", $e->getMessage());
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var \Drupal\node\Entity\Node $node */
|
||||
$this->assertInstanceOf(Node::class, $node);
|
||||
$this->assertSame('Testing article', $node->getTitle());
|
||||
if (!empty($expected['user_id'])) {
|
||||
$owner = $node->getOwner();
|
||||
$this->assertEquals($expected['user_id'], $owner->id());
|
||||
}
|
||||
$tags = $node->get('field_tags')->getValue();
|
||||
if (!empty($expected['tag_ids'][0])) {
|
||||
$this->assertEquals($expected['tag_ids'][0], $tags[0]['target_id']);
|
||||
}
|
||||
else {
|
||||
$this->assertArrayNotHasKey(0, $tags);
|
||||
}
|
||||
if (!empty($expected['tag_ids'][1])) {
|
||||
$this->assertEquals($expected['tag_ids'][1], $tags[1]['target_id']);
|
||||
}
|
||||
else {
|
||||
$this->assertArrayNotHasKey(1, $tags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests denormalization for related resources with missing or invalid types.
|
||||
*/
|
||||
public function testDenormalizeInvalidTypeAndNoType(): void {
|
||||
$payload_data = [
|
||||
'data' => [
|
||||
'type' => 'node--article',
|
||||
'attributes' => [
|
||||
'title' => 'Testing article',
|
||||
'id' => '33095485-70D2-4E51-A309-535CC5BC0115',
|
||||
],
|
||||
'relationships' => [
|
||||
'uid' => [
|
||||
'data' => [
|
||||
'type' => 'user--user',
|
||||
'id' => $this->user2->uuid(),
|
||||
],
|
||||
],
|
||||
'field_tags' => [
|
||||
'data' => [
|
||||
[
|
||||
'type' => 'foobar',
|
||||
'id' => $this->term1->uuid(),
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// Test relationship member with invalid type.
|
||||
$payload = Json::encode($payload_data);
|
||||
$resource_type = $this->container->get('jsonapi.resource_type.repository')->get('node', 'article');
|
||||
try {
|
||||
$this
|
||||
->getNormalizer()
|
||||
->denormalize(Json::decode($payload), NULL, 'api_json', [
|
||||
'resource_type' => $resource_type,
|
||||
]);
|
||||
|
||||
$this->fail('No assertion thrown for invalid type');
|
||||
}
|
||||
catch (BadRequestHttpException $e) {
|
||||
$this->assertEquals("Invalid type specified for related resource: 'foobar'", $e->getMessage());
|
||||
}
|
||||
|
||||
// Test relationship member with no type.
|
||||
unset($payload_data['data']['relationships']['field_tags']['data'][0]['type']);
|
||||
|
||||
$payload = Json::encode($payload_data);
|
||||
$resource_type = $this->container->get('jsonapi.resource_type.repository')->get('node', 'article');
|
||||
try {
|
||||
$this->container->get('jsonapi_test_normalizers_kernel.jsonapi_document_toplevel')
|
||||
->denormalize(Json::decode($payload), NULL, 'api_json', [
|
||||
'resource_type' => $resource_type,
|
||||
]);
|
||||
|
||||
$this->fail('No assertion thrown for missing type');
|
||||
}
|
||||
catch (BadRequestHttpException $e) {
|
||||
$this->assertEquals("No type specified for related resource", $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We cannot use a PHPUnit data provider because our data depends on $this.
|
||||
*
|
||||
* @param array $options
|
||||
* Options for how to construct test data.
|
||||
*
|
||||
* @return array
|
||||
* The test data.
|
||||
*/
|
||||
protected function denormalizeUuidProviderBuilder(array $options) {
|
||||
[$input, $expected] = $options;
|
||||
[$input_tag_uuids, $input_user_uuid] = $input;
|
||||
[$expected_tag_ids, $expected_user_id] = $expected;
|
||||
|
||||
$node = [
|
||||
[
|
||||
'data' => [
|
||||
'type' => 'node--article',
|
||||
'attributes' => [
|
||||
'title' => 'Testing article',
|
||||
],
|
||||
'relationships' => [
|
||||
'uid' => [
|
||||
'data' => [
|
||||
'type' => 'user--user',
|
||||
'id' => $input_user_uuid,
|
||||
],
|
||||
],
|
||||
'field_tags' => [
|
||||
'data' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'tag_ids' => $expected_tag_ids,
|
||||
'user_id' => $expected_user_id,
|
||||
],
|
||||
];
|
||||
|
||||
if (isset($input_tag_uuids[0])) {
|
||||
$node[0]['data']['relationships']['field_tags']['data'][0] = [
|
||||
'type' => 'taxonomy_term--tags',
|
||||
'id' => $input_tag_uuids[0],
|
||||
];
|
||||
}
|
||||
if (isset($input_tag_uuids[1])) {
|
||||
$node[0]['data']['relationships']['field_tags']['data'][1] = [
|
||||
'type' => 'taxonomy_term--tags',
|
||||
'id' => $input_tag_uuids[1],
|
||||
];
|
||||
}
|
||||
return $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that cacheability metadata is properly added.
|
||||
*
|
||||
* @param \Drupal\Core\Cache\CacheableMetadata $expected_metadata
|
||||
* The expected cacheable metadata.
|
||||
*
|
||||
* @dataProvider testCacheableMetadataProvider
|
||||
*/
|
||||
public function testCacheableMetadata(CacheableMetadata $expected_metadata): void {
|
||||
$resource_type = $this->container->get('jsonapi.resource_type.repository')->get('node', 'article');
|
||||
$resource_object = ResourceObject::createFromEntity($resource_type, $this->node);
|
||||
$context = [
|
||||
'resource_type' => $resource_type,
|
||||
'account' => NULL,
|
||||
];
|
||||
$jsonapi_doc_object = $this->getNormalizer()->normalize(new JsonApiDocumentTopLevel(new ResourceObjectData([$resource_object], 1), new NullIncludedData(), new LinkCollection([])), 'api_json', $context);
|
||||
foreach ($expected_metadata->getCacheTags() as $tag) {
|
||||
$this->assertContains($tag, $jsonapi_doc_object->getCacheTags());
|
||||
}
|
||||
foreach ($expected_metadata->getCacheContexts() as $context) {
|
||||
$this->assertContains($context, $jsonapi_doc_object->getCacheContexts());
|
||||
}
|
||||
$this->assertSame($expected_metadata->getCacheMaxAge(), $jsonapi_doc_object->getCacheMaxAge());
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides test cases for asserting cacheable metadata behavior.
|
||||
*/
|
||||
public static function testCacheableMetadataProvider(): array {
|
||||
$cacheable_metadata = function ($metadata) {
|
||||
return CacheableMetadata::createFromRenderArray(['#cache' => $metadata]);
|
||||
};
|
||||
|
||||
return [
|
||||
[
|
||||
$cacheable_metadata(['contexts' => ['languages:language_interface']]),
|
||||
['node--article' => 'body'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to load the normalizer.
|
||||
*/
|
||||
protected function getNormalizer(): JsonApiDocumentTopLevelNormalizer {
|
||||
$normalizer_service = $this->container->get('jsonapi_test_normalizers_kernel.jsonapi_document_toplevel');
|
||||
// Simulate what happens when this normalizer service is used via the
|
||||
// serializer service, as it is meant to be used.
|
||||
$normalizer_service->setSerializer($this->container->get('jsonapi.serializer'));
|
||||
return $normalizer_service;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function jsonSchemaDataProvider(): array {
|
||||
return [
|
||||
'Empty collection top-level document' => [
|
||||
new JsonApiDocumentTopLevel(
|
||||
new ResourceObjectData([]),
|
||||
new NullIncludedData(),
|
||||
new LinkCollection([])
|
||||
),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the generated resource object normalization against the schema.
|
||||
*
|
||||
* @covers \Drupal\jsonapi\Normalizer\ResourceObjectNormalizer::normalize
|
||||
* @covers \Drupal\jsonapi\Normalizer\ResourceObjectNormalizer::getNormalizationSchema
|
||||
*/
|
||||
public function testResourceObjectSchema(): void {
|
||||
[, $resource_object] = $this->getTestContentEntityResource();
|
||||
$serializer = $this->container->get('jsonapi.serializer');
|
||||
$context = ['account' => NULL];
|
||||
$format = $this->getJsonSchemaTestNormalizationFormat();
|
||||
$schema = $serializer->normalize($resource_object, 'json_schema', $context);
|
||||
$this->doCheckSchemaAgainstMetaSchema($schema);
|
||||
$normalized = json_decode(json_encode($serializer->normalize(
|
||||
$resource_object,
|
||||
$format,
|
||||
$context
|
||||
)->getNormalization()));
|
||||
$validator = $this->getValidator();
|
||||
$validator->validate($normalized, json_decode(json_encode($schema)));
|
||||
$this->assertSame([], $validator->getErrors(), 'Validation errors on object ' . print_r($normalized, TRUE) . ' with schema ' . print_r($schema, TRUE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the generated config resource object normalization against the schema.
|
||||
*
|
||||
* @covers \Drupal\jsonapi\Normalizer\ResourceObjectNormalizer::normalize
|
||||
* @covers \Drupal\jsonapi\Normalizer\ResourceObjectNormalizer::getNormalizationSchema
|
||||
*/
|
||||
public function testConfigEntityResourceObjectSchema(): void {
|
||||
[, $resource_object] = $this->getTestConfigEntityResource();
|
||||
$serializer = $this->container->get('jsonapi.serializer');
|
||||
$context = ['account' => NULL];
|
||||
$format = $this->getJsonSchemaTestNormalizationFormat();
|
||||
$schema = $serializer->normalize($resource_object, 'json_schema', $context);
|
||||
$this->doCheckSchemaAgainstMetaSchema($schema);
|
||||
$normalized = json_decode(json_encode($serializer->normalize(
|
||||
$resource_object,
|
||||
$format,
|
||||
$context
|
||||
)->getNormalization()));
|
||||
$validator = $this->getValidator();
|
||||
$validator->validate($normalized, json_decode(json_encode($schema)));
|
||||
$this->assertSame([], $validator->getErrors(), 'Validation errors on object ' . print_r($normalized, TRUE) . ' with schema ' . print_r($schema, TRUE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the serialization of a top-level JSON:API document with a single resource.
|
||||
*/
|
||||
public function testTopLevelResourceWithSingleResource(): void {
|
||||
[, $resource_object] = $this->getTestContentEntityResource();
|
||||
$serializer = $this->container->get('jsonapi.serializer');
|
||||
$context = ['account' => NULL];
|
||||
$format = $this->getJsonSchemaTestNormalizationFormat();
|
||||
$topLevel = new JsonApiDocumentTopLevel(
|
||||
new ResourceObjectData([$resource_object]),
|
||||
new NullIncludedData(),
|
||||
new LinkCollection([])
|
||||
);
|
||||
$schema = $serializer->normalize($topLevel, 'json_schema', $context);
|
||||
$this->doCheckSchemaAgainstMetaSchema($schema);
|
||||
$normalized = json_decode(json_encode($serializer->normalize(
|
||||
$topLevel,
|
||||
$format,
|
||||
$context
|
||||
)->getNormalization()));
|
||||
$validator = $this->getValidator();
|
||||
$validator->validate($normalized, json_decode(json_encode($schema)));
|
||||
$this->assertSame([], $validator->getErrors(), 'Validation errors on object ' . print_r($normalized, TRUE) . ' with schema ' . print_r($schema, TRUE));
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Kernel\Normalizer;
|
||||
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\jsonapi\JsonApiResource\Link;
|
||||
use Drupal\jsonapi\JsonApiResource\LinkCollection;
|
||||
use Drupal\jsonapi\JsonApiResource\ResourceObject;
|
||||
use Drupal\jsonapi\Normalizer\LinkCollectionNormalizer;
|
||||
use Drupal\jsonapi\Normalizer\Value\CacheableNormalization;
|
||||
use Drupal\jsonapi\ResourceType\ResourceType;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\Tests\user\Traits\UserCreationTrait;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\jsonapi\Normalizer\LinkCollectionNormalizer
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class LinkCollectionNormalizerTest extends KernelTestBase {
|
||||
|
||||
use UserCreationTrait;
|
||||
|
||||
/**
|
||||
* The serializer.
|
||||
*
|
||||
* @var \Symfony\Component\Serializer\SerializerInterface
|
||||
*/
|
||||
protected $serializer;
|
||||
|
||||
/**
|
||||
* The subject under test.
|
||||
*
|
||||
* @var \Drupal\jsonapi\Normalizer\LinkCollectionNormalizer
|
||||
*/
|
||||
protected $normalizer;
|
||||
|
||||
/**
|
||||
* Test users.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface[]
|
||||
*/
|
||||
protected $testUsers;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'file',
|
||||
'jsonapi',
|
||||
'serialization',
|
||||
'system',
|
||||
'user',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
// Add the entity schemas.
|
||||
$this->installEntitySchema('user');
|
||||
// Add the additional table schemas.
|
||||
$this->installSchema('user', ['users_data']);
|
||||
// Set the user IDs to something higher than 1 so these users cannot be
|
||||
// mistaken for the site admin.
|
||||
$this->testUsers[] = $this->createUser([], NULL, FALSE, ['uid' => 2]);
|
||||
$this->testUsers[] = $this->createUser([], NULL, FALSE, ['uid' => 3]);
|
||||
$this->serializer = $this->container->get('jsonapi.serializer');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the link collection normalizer.
|
||||
*/
|
||||
public function testNormalize(): void {
|
||||
$link_context = new ResourceObject(new CacheableMetadata(), new ResourceType('n/a', 'n/a', 'n/a'), 'n/a', NULL, [], new LinkCollection([]));
|
||||
$link_collection = (new LinkCollection([]))
|
||||
->withLink('related', new Link(new CacheableMetadata(), Url::fromUri('http://example.com/post/42'), 'related', ['title' => 'Most viewed']))
|
||||
->withLink('related', new Link(new CacheableMetadata(), Url::fromUri('http://example.com/post/42'), 'related', ['title' => 'Top rated']))
|
||||
->withContext($link_context);
|
||||
// Create the SUT.
|
||||
$normalized = $this->getNormalizer()->normalize($link_collection)->getNormalization();
|
||||
$this->assertIsArray($normalized);
|
||||
foreach (array_keys($normalized) as $key) {
|
||||
$this->assertStringStartsWith('related', $key);
|
||||
}
|
||||
$this->assertSame([
|
||||
[
|
||||
'href' => 'http://example.com/post/42',
|
||||
'meta' => [
|
||||
'title' => 'Most viewed',
|
||||
],
|
||||
],
|
||||
[
|
||||
'href' => 'http://example.com/post/42',
|
||||
'meta' => [
|
||||
'title' => 'Top rated',
|
||||
],
|
||||
],
|
||||
], array_values($normalized));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the link collection normalizer.
|
||||
*
|
||||
* @dataProvider linkAccessTestData
|
||||
*/
|
||||
public function testLinkAccess($current_user_id, $edit_form_uid, $expected_link_keys, $expected_cache_contexts): void {
|
||||
// Get the current user and an edit-form URL.
|
||||
foreach ($this->testUsers as $user) {
|
||||
$uid = (int) $user->id();
|
||||
if ($uid === $current_user_id) {
|
||||
$current_user = $user;
|
||||
}
|
||||
if ($uid === $edit_form_uid) {
|
||||
$edit_form_url = $user->toUrl('edit-form');
|
||||
}
|
||||
}
|
||||
assert(isset($current_user));
|
||||
assert(isset($edit_form_url));
|
||||
|
||||
// Create a link collection to normalize.
|
||||
$mock_resource_object = $this->createMock(ResourceObject::class);
|
||||
$link_collection = new LinkCollection([
|
||||
'edit-form' => new Link(new CacheableMetadata(), $edit_form_url, 'edit-form', ['title' => 'Edit']),
|
||||
]);
|
||||
$link_collection = $link_collection->withContext($mock_resource_object);
|
||||
|
||||
// Normalize the collection.
|
||||
$actual_normalization = $this->getNormalizer($current_user)->normalize($link_collection);
|
||||
|
||||
// Check that it returned the expected value object.
|
||||
$this->assertInstanceOf(CacheableNormalization::class, $actual_normalization);
|
||||
|
||||
// Get the raw normalized data.
|
||||
$actual_data = $actual_normalization->getNormalization();
|
||||
$this->assertIsArray($actual_data);
|
||||
|
||||
// Check that the expected links are present and unexpected links are
|
||||
// absent.
|
||||
$actual_link_keys = array_keys($actual_data);
|
||||
sort($expected_link_keys);
|
||||
sort($actual_link_keys);
|
||||
$this->assertSame($expected_link_keys, $actual_link_keys);
|
||||
|
||||
// Check that the expected cache contexts were added.
|
||||
$actual_cache_contexts = $actual_normalization->getCacheContexts();
|
||||
sort($expected_cache_contexts);
|
||||
sort($actual_cache_contexts);
|
||||
$this->assertSame($expected_cache_contexts, $actual_cache_contexts);
|
||||
|
||||
// If the edit-form link was present, check that it has the correct href.
|
||||
if (isset($actual_data['edit-form'])) {
|
||||
$this->assertSame($actual_data['edit-form'], [
|
||||
'href' => $edit_form_url->setAbsolute()->toString(),
|
||||
'meta' => [
|
||||
'title' => 'Edit',
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides test cases for testing link access checking.
|
||||
*
|
||||
* @return array[]
|
||||
* An array of test cases for testLinkAccess().
|
||||
*/
|
||||
public static function linkAccessTestData() {
|
||||
return [
|
||||
'the edit-form link is present because uid 2 has access to the targeted resource (its own edit form)' => [
|
||||
'current_user_id' => 2,
|
||||
'edit_form_uid' => 2,
|
||||
'expected_link_keys' => ['edit-form'],
|
||||
'expected_cache_contexts' => ['url.site', 'user'],
|
||||
],
|
||||
"the edit-form link is omitted because uid 3 doesn't have access to the targeted resource (another account's edit form)" => [
|
||||
'current_user_id' => 3,
|
||||
'edit_form_uid' => 2,
|
||||
'expected_link_keys' => [],
|
||||
'expected_cache_contexts' => ['url.site', 'user'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance of the normalizer to test.
|
||||
*/
|
||||
protected function getNormalizer(?AccountInterface $current_user = NULL) {
|
||||
if (is_null($current_user)) {
|
||||
$current_user = $this->setUpCurrentUser();
|
||||
}
|
||||
else {
|
||||
$this->setCurrentUser($current_user);
|
||||
}
|
||||
$normalizer = new LinkCollectionNormalizer($current_user);
|
||||
$normalizer->setSerializer($this->serializer);
|
||||
return $normalizer;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,446 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Kernel\Normalizer;
|
||||
|
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\file\Entity\File;
|
||||
use Drupal\jsonapi\JsonApiResource\Relationship;
|
||||
use Drupal\jsonapi\JsonApiResource\ResourceObject;
|
||||
use Drupal\jsonapi\Normalizer\RelationshipNormalizer;
|
||||
use Drupal\jsonapi\Normalizer\Value\CacheableNormalization;
|
||||
use Drupal\jsonapi\ResourceType\ResourceType;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\Tests\jsonapi\Kernel\JsonapiKernelTestBase;
|
||||
use Drupal\Tests\user\Traits\UserCreationTrait;
|
||||
use Drupal\user\Entity\User;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\jsonapi\Normalizer\RelationshipNormalizer
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class RelationshipNormalizerTest extends JsonapiKernelTestBase {
|
||||
|
||||
use UserCreationTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'field',
|
||||
'file',
|
||||
'image',
|
||||
'jsonapi',
|
||||
'node',
|
||||
'serialization',
|
||||
'system',
|
||||
'user',
|
||||
];
|
||||
|
||||
/**
|
||||
* Static UUID for the referencing entity.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $referencerId = '2c344ae5-4303-4f17-acd4-e20d2a9a6c44';
|
||||
|
||||
/**
|
||||
* Static UUIDs for use in tests.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected static $userIds = [
|
||||
'457fed75-a3ed-4e9e-823c-f9aeff6ec8ca',
|
||||
'67e4063f-ac74-46ac-ac5f-07efda9fd551',
|
||||
];
|
||||
|
||||
/**
|
||||
* Static UIDs for use in tests.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected static $userUids = [
|
||||
10,
|
||||
11,
|
||||
];
|
||||
|
||||
/**
|
||||
* Static UUIDs for use in tests.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected static $imageIds = [
|
||||
'71e67249-df4a-4616-9065-4cc2e812235b',
|
||||
'ce5093fc-417f-477d-932d-888407d5cbd5',
|
||||
];
|
||||
/**
|
||||
* Static UUIDs for use in tests.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected static $imageUids = [
|
||||
1,
|
||||
2,
|
||||
];
|
||||
|
||||
/**
|
||||
* A user.
|
||||
*
|
||||
* @var \Drupal\user\Entity\User
|
||||
*/
|
||||
protected User $user1;
|
||||
|
||||
/**
|
||||
* A user.
|
||||
*
|
||||
* @var \Drupal\user\Entity\User
|
||||
*/
|
||||
protected User $user2;
|
||||
|
||||
/**
|
||||
* An image.
|
||||
*
|
||||
* @var \Drupal\file\Entity\File
|
||||
*/
|
||||
protected File $image1;
|
||||
|
||||
/**
|
||||
* An image.
|
||||
*
|
||||
* @var \Drupal\file\Entity\File
|
||||
*/
|
||||
protected File $image2;
|
||||
|
||||
/**
|
||||
* A referencer node.
|
||||
*
|
||||
* @var \Drupal\node\Entity\Node
|
||||
*/
|
||||
protected Node $referencer;
|
||||
|
||||
/**
|
||||
* The node type.
|
||||
*
|
||||
* @var \Drupal\jsonapi\ResourceType\ResourceType
|
||||
*/
|
||||
protected ResourceType $referencingResourceType;
|
||||
|
||||
/**
|
||||
* The normalizer.
|
||||
*
|
||||
* @var \Drupal\jsonapi\Normalizer\RelationshipNormalizer
|
||||
*/
|
||||
protected RelationshipNormalizer $normalizer;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
// Set up the data model.
|
||||
// Add the entity schemas.
|
||||
$this->installEntitySchema('node');
|
||||
$this->installEntitySchema('user');
|
||||
$this->installEntitySchema('file');
|
||||
|
||||
// Add the additional table schemas.
|
||||
$this->installSchema('node', ['node_access']);
|
||||
$this->installSchema('file', ['file_usage']);
|
||||
NodeType::create([
|
||||
'type' => 'referencer',
|
||||
'name' => 'Referencer',
|
||||
])->save();
|
||||
$this->createEntityReferenceField('node', 'referencer', 'field_user', 'User', 'user', 'default', ['target_bundles' => NULL], 1);
|
||||
$this->createEntityReferenceField('node', 'referencer', 'field_users', 'Users', 'user', 'default', ['target_bundles' => NULL], FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
|
||||
$field_storage_config = [
|
||||
'type' => 'image',
|
||||
'entity_type' => 'node',
|
||||
];
|
||||
FieldStorageConfig::create(['field_name' => 'field_image', 'cardinality' => 1] + $field_storage_config)->save();
|
||||
FieldStorageConfig::create([
|
||||
'field_name' => 'field_images',
|
||||
'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
|
||||
] + $field_storage_config)->save();
|
||||
$field_config = [
|
||||
'entity_type' => 'node',
|
||||
'bundle' => 'referencer',
|
||||
];
|
||||
FieldConfig::create(['field_name' => 'field_image', 'label' => 'Image'] + $field_config)->save();
|
||||
FieldConfig::create(['field_name' => 'field_images', 'label' => 'Images'] + $field_config)->save();
|
||||
|
||||
// Set up the test data.
|
||||
$this->setUpCurrentUser([], ['access content']);
|
||||
$this->user1 = User::create([
|
||||
'name' => $this->randomMachineName(),
|
||||
'mail' => $this->randomMachineName() . '@example.com',
|
||||
'uuid' => static::$userIds[0],
|
||||
'uid' => static::$userUids[0],
|
||||
]);
|
||||
$this->user1->save();
|
||||
$this->user2 = User::create([
|
||||
'name' => $this->randomMachineName(),
|
||||
'mail' => $this->randomMachineName() . '@example.com',
|
||||
'uuid' => static::$userIds[1],
|
||||
'uid' => static::$userUids[1],
|
||||
]);
|
||||
$this->user2->save();
|
||||
|
||||
$this->image1 = File::create([
|
||||
'uri' => 'public:/image1.png',
|
||||
'uuid' => static::$imageIds[0],
|
||||
'uid' => static::$imageUids[0],
|
||||
]);
|
||||
$this->image1->save();
|
||||
$this->image2 = File::create([
|
||||
'uri' => 'public:/image2.png',
|
||||
'uuid' => static::$imageIds[1],
|
||||
'uid' => static::$imageUids[1],
|
||||
]);
|
||||
$this->image2->save();
|
||||
|
||||
// Create the node from which all the previously created entities will be
|
||||
// referenced.
|
||||
$this->referencer = Node::create([
|
||||
'title' => 'Referencing node',
|
||||
'type' => 'referencer',
|
||||
'status' => 1,
|
||||
'uuid' => static::$referencerId,
|
||||
]);
|
||||
$this->referencer->save();
|
||||
|
||||
// Set up the test dependencies.
|
||||
$resource_type_repository = $this->container->get('jsonapi.resource_type.repository');
|
||||
$this->referencingResourceType = $resource_type_repository->get('node', 'referencer');
|
||||
$this->normalizer = new RelationshipNormalizer($resource_type_repository);
|
||||
$this->normalizer->setSerializer($this->container->get('jsonapi.serializer'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::normalize
|
||||
* @dataProvider normalizeProvider
|
||||
*/
|
||||
public function testNormalize($entity_property_names, $field_name, $expected): void {
|
||||
// Links cannot be generated in the test provider because the container
|
||||
// has not yet been set.
|
||||
$expected['links'] = [
|
||||
'self' => ['href' => Url::fromUri('base:/jsonapi/node/referencer/' . static::$referencerId . "/relationships/$field_name", ['query' => ['resourceVersion' => 'id:1']])->setAbsolute()->toString()],
|
||||
'related' => ['href' => Url::fromUri('base:/jsonapi/node/referencer/' . static::$referencerId . "/$field_name", ['query' => ['resourceVersion' => 'id:1']])->setAbsolute()->toString()],
|
||||
];
|
||||
// Set up different field values.
|
||||
$this->referencer->{$field_name} = array_map(function ($entity_property_name) {
|
||||
$value = ['target_id' => $this->{$entity_property_name === 'image1a' ? 'image1' : $entity_property_name}->id()];
|
||||
switch ($entity_property_name) {
|
||||
case 'image1':
|
||||
$value['alt'] = 'Cute llama';
|
||||
$value['title'] = 'My spirit animal';
|
||||
break;
|
||||
|
||||
case 'image1a':
|
||||
$value['alt'] = 'Ugly llama';
|
||||
$value['title'] = 'My alter ego';
|
||||
break;
|
||||
|
||||
case 'image2':
|
||||
$value['alt'] = 'Adorable llama';
|
||||
$value['title'] = 'My spirit animal 😍';
|
||||
break;
|
||||
}
|
||||
return $value;
|
||||
}, $entity_property_names);
|
||||
$resource_object = ResourceObject::createFromEntity($this->referencingResourceType, $this->referencer);
|
||||
$relationship = Relationship::createFromEntityReferenceField($resource_object, $resource_object->getField($field_name));
|
||||
// Normalize.
|
||||
$actual = $this->normalizer->normalize($relationship, 'api_json');
|
||||
// Assert.
|
||||
assert($actual instanceof CacheableNormalization);
|
||||
$this->assertEquals($expected, $actual->getNormalization());
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for testNormalize.
|
||||
*/
|
||||
public static function normalizeProvider() {
|
||||
return [
|
||||
'single cardinality' => [
|
||||
['user1'],
|
||||
'field_user',
|
||||
[
|
||||
'data' => [
|
||||
'type' => 'user--user',
|
||||
'id' => static::$userIds[0],
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => static::$userUids[0],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'multiple cardinality' => [
|
||||
['user1', 'user2'], 'field_users', [
|
||||
'data' => [
|
||||
[
|
||||
'type' => 'user--user',
|
||||
'id' => static::$userIds[0],
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => static::$userUids[0],
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'user--user',
|
||||
'id' => static::$userIds[1],
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => static::$userUids[1],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'multiple cardinality, all same values' => [
|
||||
['user1', 'user1'], 'field_users', [
|
||||
'data' => [
|
||||
[
|
||||
'type' => 'user--user',
|
||||
'id' => static::$userIds[0],
|
||||
'meta' => [
|
||||
'arity' => 0,
|
||||
'drupal_internal__target_id' => static::$userUids[0],
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'user--user',
|
||||
'id' => static::$userIds[0],
|
||||
'meta' => [
|
||||
'arity' => 1,
|
||||
'drupal_internal__target_id' => static::$userUids[0],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'multiple cardinality, some same values' => [
|
||||
['user1', 'user2', 'user1'], 'field_users', [
|
||||
'data' => [
|
||||
[
|
||||
'type' => 'user--user',
|
||||
'id' => static::$userIds[0],
|
||||
'meta' => [
|
||||
'arity' => 0,
|
||||
'drupal_internal__target_id' => static::$userUids[0],
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'user--user',
|
||||
'id' => static::$userIds[1],
|
||||
'meta' => [
|
||||
'drupal_internal__target_id' => static::$userUids[1],
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'user--user',
|
||||
'id' => static::$userIds[0],
|
||||
'meta' => [
|
||||
'arity' => 1,
|
||||
'drupal_internal__target_id' => static::$userUids[0],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'single cardinality, with meta' => [
|
||||
['image1'], 'field_image', [
|
||||
'data' => [
|
||||
'type' => 'file--file',
|
||||
'id' => static::$imageIds[0],
|
||||
'meta' => [
|
||||
'alt' => 'Cute llama',
|
||||
'title' => 'My spirit animal',
|
||||
'width' => NULL,
|
||||
'height' => NULL,
|
||||
'drupal_internal__target_id' => static::$imageUids[0],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'multiple cardinality, all same values, with meta' => [
|
||||
['image1', 'image1'], 'field_images', [
|
||||
'data' => [
|
||||
[
|
||||
'type' => 'file--file',
|
||||
'id' => static::$imageIds[0],
|
||||
'meta' => [
|
||||
'alt' => 'Cute llama',
|
||||
'title' => 'My spirit animal',
|
||||
'width' => NULL,
|
||||
'height' => NULL,
|
||||
'arity' => 0,
|
||||
'drupal_internal__target_id' => static::$imageUids[0],
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'file--file',
|
||||
'id' => static::$imageIds[0],
|
||||
'meta' => [
|
||||
'alt' => 'Cute llama',
|
||||
'title' => 'My spirit animal',
|
||||
'width' => NULL,
|
||||
'height' => NULL,
|
||||
'arity' => 1,
|
||||
'drupal_internal__target_id' => static::$imageUids[0],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'multiple cardinality, some same values with same values but different meta' => [
|
||||
['image1', 'image1', 'image1a'], 'field_images', [
|
||||
'data' => [
|
||||
[
|
||||
'type' => 'file--file',
|
||||
'id' => static::$imageIds[0],
|
||||
'meta' => [
|
||||
'alt' => 'Cute llama',
|
||||
'title' => 'My spirit animal',
|
||||
'width' => NULL,
|
||||
'height' => NULL,
|
||||
'arity' => 0,
|
||||
'drupal_internal__target_id' => static::$imageUids[0],
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'file--file',
|
||||
'id' => static::$imageIds[0],
|
||||
'meta' => [
|
||||
'alt' => 'Cute llama',
|
||||
'title' => 'My spirit animal',
|
||||
'width' => NULL,
|
||||
'height' => NULL,
|
||||
'arity' => 1,
|
||||
'drupal_internal__target_id' => static::$imageUids[0],
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'file--file',
|
||||
'id' => static::$imageIds[0],
|
||||
'meta' => [
|
||||
'alt' => 'Ugly llama',
|
||||
'title' => 'My alter ego',
|
||||
'width' => NULL,
|
||||
'height' => NULL,
|
||||
'arity' => 2,
|
||||
'drupal_internal__target_id' => static::$imageUids[0],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
424
web/core/modules/jsonapi/tests/src/Kernel/Query/FilterTest.php
Normal file
424
web/core/modules/jsonapi/tests/src/Kernel/Query/FilterTest.php
Normal file
@ -0,0 +1,424 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Kernel\Query;
|
||||
|
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
||||
use Drupal\Core\Http\Exception\CacheableBadRequestHttpException;
|
||||
use Drupal\jsonapi\Context\FieldResolver;
|
||||
use Drupal\jsonapi\Query\Filter;
|
||||
use Drupal\jsonapi\ResourceType\ResourceType;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\Tests\image\Kernel\ImageFieldCreationTrait;
|
||||
use Drupal\Tests\jsonapi\Kernel\JsonapiKernelTestBase;
|
||||
use Prophecy\Argument;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\jsonapi\Query\Filter
|
||||
* @group jsonapi
|
||||
* @group jsonapi_query
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class FilterTest extends JsonapiKernelTestBase {
|
||||
|
||||
use ImageFieldCreationTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'field',
|
||||
'file',
|
||||
'image',
|
||||
'jsonapi',
|
||||
'node',
|
||||
'serialization',
|
||||
'system',
|
||||
'text',
|
||||
'user',
|
||||
];
|
||||
|
||||
/**
|
||||
* A node storage instance.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityStorageInterface
|
||||
*/
|
||||
protected $nodeStorage;
|
||||
|
||||
/**
|
||||
* The JSON:API resource type repository.
|
||||
*
|
||||
* @var \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface
|
||||
*/
|
||||
protected $resourceTypeRepository;
|
||||
|
||||
/**
|
||||
* @var \Drupal\jsonapi\Context\FieldResolver
|
||||
*/
|
||||
protected FieldResolver $fieldResolver;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->setUpSchemas();
|
||||
|
||||
$this->savePaintingType();
|
||||
|
||||
// ((RED or CIRCLE) or (YELLOW and SQUARE))
|
||||
$this->savePaintings([
|
||||
['colors' => ['red'], 'shapes' => ['triangle'], 'title' => 'FIND'],
|
||||
['colors' => ['orange'], 'shapes' => ['circle'], 'title' => 'FIND'],
|
||||
['colors' => ['orange'], 'shapes' => ['triangle'], 'title' => 'DO_NOT_FIND'],
|
||||
['colors' => ['yellow'], 'shapes' => ['square'], 'title' => 'FIND'],
|
||||
['colors' => ['yellow'], 'shapes' => ['triangle'], 'title' => 'DO_NOT_FIND'],
|
||||
['colors' => ['orange'], 'shapes' => ['square'], 'title' => 'DO_NOT_FIND'],
|
||||
]);
|
||||
|
||||
$this->nodeStorage = $this->container->get('entity_type.manager')->getStorage('node');
|
||||
$this->fieldResolver = $this->container->get('jsonapi.field_resolver');
|
||||
$this->resourceTypeRepository = $this->container->get('jsonapi.resource_type.repository');
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::queryCondition
|
||||
*/
|
||||
public function testInvalidFilterPathDueToMissingPropertyName(): void {
|
||||
$this->expectException(CacheableBadRequestHttpException::class);
|
||||
$this->expectExceptionMessage('Invalid nested filtering. The field `colors`, given in the path `colors` is incomplete, it must end with one of the following specifiers: `value`, `format`, `processed`.');
|
||||
$resource_type = $this->resourceTypeRepository->get('node', 'painting');
|
||||
Filter::createFromQueryParameter(['colors' => ''], $resource_type, $this->fieldResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::queryCondition
|
||||
*/
|
||||
public function testInvalidFilterPathDueToMissingPropertyNameReferenceFieldWithMetaProperties(): void {
|
||||
$this->expectException(CacheableBadRequestHttpException::class);
|
||||
$this->expectExceptionMessage('Invalid nested filtering. The field `photo`, given in the path `photo` is incomplete, it must end with one of the following specifiers: `id`, `meta.drupal_internal__target_id`, `meta.alt`, `meta.title`, `meta.width`, `meta.height`.');
|
||||
$resource_type = $this->resourceTypeRepository->get('node', 'painting');
|
||||
Filter::createFromQueryParameter(['photo' => ''], $resource_type, $this->fieldResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::queryCondition
|
||||
*/
|
||||
public function testInvalidFilterPathDueMissingMetaPrefixReferenceFieldWithMetaProperties(): void {
|
||||
$this->expectException(CacheableBadRequestHttpException::class);
|
||||
$this->expectExceptionMessage('Invalid nested filtering. The property `alt`, given in the path `photo.alt` belongs to the meta object of a relationship and must be preceded by `meta`.');
|
||||
$resource_type = $this->resourceTypeRepository->get('node', 'painting');
|
||||
Filter::createFromQueryParameter(['photo.alt' => ''], $resource_type, $this->fieldResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::queryCondition
|
||||
*/
|
||||
public function testInvalidFilterPathDueToMissingPropertyNameReferenceFieldWithoutMetaProperties(): void {
|
||||
$this->expectException(CacheableBadRequestHttpException::class);
|
||||
$this->expectExceptionMessage('Invalid nested filtering. The field `uid`, given in the path `uid` is incomplete, it must end with one of the following specifiers: `id`, `meta.drupal_internal__target_id`.');
|
||||
$resource_type = $this->resourceTypeRepository->get('node', 'painting');
|
||||
Filter::createFromQueryParameter(['uid' => ''], $resource_type, $this->fieldResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::queryCondition
|
||||
*/
|
||||
public function testInvalidFilterPathDueToNonexistentProperty(): void {
|
||||
$this->expectException(CacheableBadRequestHttpException::class);
|
||||
$this->expectExceptionMessage('Invalid nested filtering. The property `foobar`, given in the path `colors.foobar`, does not exist. Must be one of the following property names: `value`, `format`, `processed`.');
|
||||
$resource_type = $this->resourceTypeRepository->get('node', 'painting');
|
||||
Filter::createFromQueryParameter(['colors.foobar' => ''], $resource_type, $this->fieldResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::queryCondition
|
||||
*/
|
||||
public function testInvalidFilterPathDueToElidedSoleProperty(): void {
|
||||
$this->expectException(CacheableBadRequestHttpException::class);
|
||||
$this->expectExceptionMessage('Invalid nested filtering. The property `value`, given in the path `promote.value`, does not exist. Filter by `promote`, not `promote.value` (the JSON:API module elides property names from single-property fields).');
|
||||
$resource_type = $this->resourceTypeRepository->get('node', 'painting');
|
||||
Filter::createFromQueryParameter(['promote.value' => ''], $resource_type, $this->fieldResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::queryCondition
|
||||
*/
|
||||
public function testQueryCondition(): void {
|
||||
// Can't use a data provider because we need access to the container.
|
||||
$data = $this->queryConditionData();
|
||||
|
||||
$get_sql_query_for_entity_query = function ($entity_query) {
|
||||
// Expose parts of \Drupal\Core\Entity\Query\Sql\Query::execute().
|
||||
$o = new \ReflectionObject($entity_query);
|
||||
$m1 = $o->getMethod('prepare');
|
||||
$m2 = $o->getMethod('compile');
|
||||
|
||||
// The private property computed by the two previous private calls, whose
|
||||
// value we need to inspect.
|
||||
$p = $o->getProperty('sqlQuery');
|
||||
|
||||
$m1->invoke($entity_query);
|
||||
$m2->invoke($entity_query);
|
||||
return (string) $p->getValue($entity_query);
|
||||
};
|
||||
|
||||
$resource_type = $this->resourceTypeRepository->get('node', 'painting');
|
||||
foreach ($data as $case) {
|
||||
$parameter = $case[0];
|
||||
$expected_query = $case[1];
|
||||
$filter = Filter::createFromQueryParameter($parameter, $resource_type, $this->fieldResolver);
|
||||
|
||||
$query = $this->nodeStorage->getQuery()->accessCheck(FALSE);
|
||||
|
||||
// Get the query condition parsed from the input.
|
||||
$condition = $filter->queryCondition($query);
|
||||
|
||||
// Apply it to the query.
|
||||
$query->condition($condition);
|
||||
|
||||
// Verify the SQL query is exactly the same.
|
||||
$expected_sql_query = $get_sql_query_for_entity_query($expected_query);
|
||||
$actual_sql_query = $get_sql_query_for_entity_query($query);
|
||||
$this->assertSame($expected_sql_query, $actual_sql_query);
|
||||
|
||||
// Compare the results.
|
||||
$this->assertEquals($expected_query->execute(), $query->execute());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simply provides test data to keep the actual test method tidy.
|
||||
*/
|
||||
protected function queryConditionData(): array {
|
||||
// ((RED or CIRCLE) or (YELLOW and SQUARE))
|
||||
$query = $this->nodeStorage->getQuery()->accessCheck(FALSE);
|
||||
|
||||
$or_group = $query->orConditionGroup();
|
||||
|
||||
$nested_or_group = $query->orConditionGroup();
|
||||
$nested_or_group->condition('colors', 'red', 'CONTAINS');
|
||||
$nested_or_group->condition('shapes', 'circle', 'CONTAINS');
|
||||
$or_group->condition($nested_or_group);
|
||||
|
||||
$nested_and_group = $query->andConditionGroup();
|
||||
$nested_and_group->condition('colors', 'yellow', 'CONTAINS');
|
||||
$nested_and_group->condition('shapes', 'square', 'CONTAINS');
|
||||
$nested_and_group->notExists('photo.alt');
|
||||
$or_group->condition($nested_and_group);
|
||||
|
||||
$query->condition($or_group);
|
||||
|
||||
return [
|
||||
[
|
||||
[
|
||||
'or-group' => ['group' => ['conjunction' => 'OR']],
|
||||
'nested-or-group' => ['group' => ['conjunction' => 'OR', 'memberOf' => 'or-group']],
|
||||
'nested-and-group' => ['group' => ['conjunction' => 'AND', 'memberOf' => 'or-group']],
|
||||
'condition-0' => [
|
||||
'condition' => [
|
||||
'path' => 'colors.value',
|
||||
'value' => 'red',
|
||||
'operator' => 'CONTAINS',
|
||||
'memberOf' => 'nested-or-group',
|
||||
],
|
||||
],
|
||||
'condition-1' => [
|
||||
'condition' => [
|
||||
'path' => 'shapes.value',
|
||||
'value' => 'circle',
|
||||
'operator' => 'CONTAINS',
|
||||
'memberOf' => 'nested-or-group',
|
||||
],
|
||||
],
|
||||
'condition-2' => [
|
||||
'condition' => [
|
||||
'path' => 'colors.value',
|
||||
'value' => 'yellow',
|
||||
'operator' =>
|
||||
'CONTAINS',
|
||||
'memberOf' => 'nested-and-group',
|
||||
],
|
||||
],
|
||||
'condition-3' => [
|
||||
'condition' => [
|
||||
'path' => 'shapes.value',
|
||||
'value' => 'square',
|
||||
'operator' => 'CONTAINS',
|
||||
'memberOf' => 'nested-and-group',
|
||||
],
|
||||
],
|
||||
'condition-4' => [
|
||||
'condition' => [
|
||||
'path' => 'photo.meta.alt',
|
||||
'operator' => 'IS NULL',
|
||||
'memberOf' => 'nested-and-group',
|
||||
],
|
||||
],
|
||||
],
|
||||
$query,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the schemas.
|
||||
*/
|
||||
protected function setUpSchemas(): void {
|
||||
$this->installSchema('node', ['node_access']);
|
||||
$this->installSchema('user', ['users_data']);
|
||||
|
||||
$this->installSchema('user', []);
|
||||
foreach (['user', 'node'] as $entity_type_id) {
|
||||
$this->installEntitySchema($entity_type_id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a painting node type.
|
||||
*/
|
||||
protected function savePaintingType(): void {
|
||||
NodeType::create([
|
||||
'type' => 'painting',
|
||||
'name' => 'Painting',
|
||||
])->save();
|
||||
$this->createTextField(
|
||||
'node', 'painting',
|
||||
'colors', 'Colors',
|
||||
FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
|
||||
);
|
||||
$this->createTextField(
|
||||
'node', 'painting',
|
||||
'shapes', 'Shapes',
|
||||
FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
|
||||
);
|
||||
$this->createImageField('photo', 'node', 'painting');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates painting nodes.
|
||||
*/
|
||||
protected function savePaintings($paintings): void {
|
||||
foreach ($paintings as $painting) {
|
||||
Node::create(array_merge([
|
||||
'type' => 'painting',
|
||||
], $painting))->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::createFromQueryParameter
|
||||
* @dataProvider parameterProvider
|
||||
*/
|
||||
public function testCreateFromQueryParameter($case, $expected): void {
|
||||
$resource_type = new ResourceType('foo', 'bar', NULL);
|
||||
$actual = Filter::createFromQueryParameter($case, $resource_type, $this->getFieldResolverMock($resource_type));
|
||||
$conditions = $actual->root()->members();
|
||||
for ($i = 0; $i < count($case); $i++) {
|
||||
$this->assertEquals($expected[$i]['path'], $conditions[$i]->field());
|
||||
$this->assertEquals($expected[$i]['value'], $conditions[$i]->value());
|
||||
$this->assertEquals($expected[$i]['operator'], $conditions[$i]->operator());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for testCreateFromQueryParameter.
|
||||
*/
|
||||
public static function parameterProvider() {
|
||||
return [
|
||||
'shorthand' => [
|
||||
['uid' => ['value' => 1]],
|
||||
[['path' => 'uid', 'value' => 1, 'operator' => '=']],
|
||||
],
|
||||
'extreme shorthand' => [
|
||||
['uid' => 1],
|
||||
[['path' => 'uid', 'value' => 1, 'operator' => '=']],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::createFromQueryParameter
|
||||
*/
|
||||
public function testCreateFromQueryParameterNested(): void {
|
||||
$parameter = [
|
||||
'or-group' => ['group' => ['conjunction' => 'OR']],
|
||||
'nested-or-group' => [
|
||||
'group' => ['conjunction' => 'OR', 'memberOf' => 'or-group'],
|
||||
],
|
||||
'nested-and-group' => [
|
||||
'group' => ['conjunction' => 'AND', 'memberOf' => 'or-group'],
|
||||
],
|
||||
'condition-0' => [
|
||||
'condition' => [
|
||||
'path' => 'field0',
|
||||
'value' => 'value0',
|
||||
'memberOf' => 'nested-or-group',
|
||||
],
|
||||
],
|
||||
'condition-1' => [
|
||||
'condition' => [
|
||||
'path' => 'field1',
|
||||
'value' => 'value1',
|
||||
'memberOf' => 'nested-or-group',
|
||||
],
|
||||
],
|
||||
'condition-2' => [
|
||||
'condition' => [
|
||||
'path' => 'field2',
|
||||
'value' => 'value2',
|
||||
'memberOf' => 'nested-and-group',
|
||||
],
|
||||
],
|
||||
'condition-3' => [
|
||||
'condition' => [
|
||||
'path' => 'field3',
|
||||
'value' => 'value3',
|
||||
'memberOf' => 'nested-and-group',
|
||||
],
|
||||
],
|
||||
];
|
||||
$resource_type = new ResourceType('foo', 'bar', NULL);
|
||||
$filter = Filter::createFromQueryParameter($parameter, $resource_type, $this->getFieldResolverMock($resource_type));
|
||||
$root = $filter->root();
|
||||
|
||||
// Make sure the implicit root group was added.
|
||||
$this->assertEquals('AND', $root->conjunction());
|
||||
|
||||
// Ensure the or-group and the and-group were added correctly.
|
||||
$members = $root->members();
|
||||
|
||||
// Ensure the OR group was added.
|
||||
$or_group = $members[0];
|
||||
$this->assertEquals('OR', $or_group->conjunction());
|
||||
$or_group_members = $or_group->members();
|
||||
|
||||
// Make sure the nested OR group was added with the right conditions.
|
||||
$nested_or_group = $or_group_members[0];
|
||||
$this->assertEquals('OR', $nested_or_group->conjunction());
|
||||
$nested_or_group_members = $nested_or_group->members();
|
||||
$this->assertEquals('field0', $nested_or_group_members[0]->field());
|
||||
$this->assertEquals('field1', $nested_or_group_members[1]->field());
|
||||
|
||||
// Make sure the nested AND group was added with the right conditions.
|
||||
$nested_and_group = $or_group_members[1];
|
||||
$this->assertEquals('AND', $nested_and_group->conjunction());
|
||||
$nested_and_group_members = $nested_and_group->members();
|
||||
$this->assertEquals('field2', $nested_and_group_members[0]->field());
|
||||
$this->assertEquals('field3', $nested_and_group_members[1]->field());
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a mock field resolver.
|
||||
*/
|
||||
protected function getFieldResolverMock(ResourceType $resource_type) {
|
||||
$field_resolver = $this->prophesize(FieldResolver::class);
|
||||
$field_resolver->resolveInternalEntityQueryPath($resource_type, Argument::any(), Argument::any())->willReturnArgument(1);
|
||||
return $field_resolver->reveal();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,225 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Kernel\ResourceType;
|
||||
|
||||
use Drupal\Core\Database\Database;
|
||||
use Drupal\Tests\jsonapi\Kernel\JsonapiKernelTestBase;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\jsonapi\ResourceType\ResourceType
|
||||
* @coversClass \Drupal\jsonapi\ResourceType\ResourceTypeRepository
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class RelatedResourceTypesTest extends JsonapiKernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'file',
|
||||
'node',
|
||||
'jsonapi',
|
||||
'serialization',
|
||||
'system',
|
||||
'user',
|
||||
'field',
|
||||
'dblog',
|
||||
];
|
||||
|
||||
/**
|
||||
* The JSON:API resource type repository under test.
|
||||
*
|
||||
* @var \Drupal\jsonapi\ResourceType\ResourceTypeRepository
|
||||
*/
|
||||
protected $resourceTypeRepository;
|
||||
|
||||
/**
|
||||
* The JSON:API resource type for `node--foo`.
|
||||
*
|
||||
* @var \Drupal\jsonapi\ResourceType\ResourceType
|
||||
*/
|
||||
protected $fooType;
|
||||
|
||||
/**
|
||||
* The JSON:API resource type for `node--bar`.
|
||||
*
|
||||
* @var \Drupal\jsonapi\ResourceType\ResourceType
|
||||
*/
|
||||
protected $barType;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
// Add the entity schemas.
|
||||
$this->installEntitySchema('node');
|
||||
$this->installEntitySchema('user');
|
||||
|
||||
// Add the additional table schemas.
|
||||
$this->installSchema('node', ['node_access']);
|
||||
$this->installSchema('user', ['users_data']);
|
||||
$this->installSchema('dblog', ['watchdog']);
|
||||
|
||||
NodeType::create([
|
||||
'type' => 'foo',
|
||||
'name' => 'Foo',
|
||||
])->save();
|
||||
|
||||
NodeType::create([
|
||||
'type' => 'bar',
|
||||
'name' => 'Bar',
|
||||
])->save();
|
||||
|
||||
$this->createEntityReferenceField(
|
||||
'node',
|
||||
'foo',
|
||||
'field_ref_bar',
|
||||
'Bar Reference',
|
||||
'node',
|
||||
'default',
|
||||
['target_bundles' => ['bar']]
|
||||
);
|
||||
|
||||
$this->createEntityReferenceField(
|
||||
'node',
|
||||
'foo',
|
||||
'field_ref_foo',
|
||||
'Foo Reference',
|
||||
'node',
|
||||
'default',
|
||||
// Important to test self-referencing resource types.
|
||||
['target_bundles' => ['foo']]
|
||||
);
|
||||
|
||||
$this->createEntityReferenceField(
|
||||
'node',
|
||||
'foo',
|
||||
'field_ref_any',
|
||||
'Any Bundle Reference',
|
||||
'node',
|
||||
'default',
|
||||
// This should result in a reference to any bundle.
|
||||
['target_bundles' => NULL]
|
||||
);
|
||||
|
||||
$this->resourceTypeRepository = $this->container->get('jsonapi.resource_type.repository');
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getRelatableResourceTypes
|
||||
* @dataProvider getRelatableResourceTypesProvider
|
||||
*/
|
||||
public function testGetRelatableResourceTypes($resource_type_name, $relatable_type_names): void {
|
||||
// We're only testing the fields that we set up.
|
||||
$test_fields = [
|
||||
'field_ref_foo',
|
||||
'field_ref_bar',
|
||||
'field_ref_any',
|
||||
];
|
||||
|
||||
$resource_type = $this->resourceTypeRepository->getByTypeName($resource_type_name);
|
||||
|
||||
// This extracts just the relationship fields under test.
|
||||
$subjects = array_intersect_key(
|
||||
$resource_type->getRelatableResourceTypes(),
|
||||
array_flip($test_fields)
|
||||
);
|
||||
|
||||
// Map the related resource type to their type name so we can just compare
|
||||
// the type names rather that the whole object.
|
||||
foreach ($test_fields as $field_name) {
|
||||
if (isset($subjects[$field_name])) {
|
||||
$subjects[$field_name] = array_map(function ($resource_type) {
|
||||
return $resource_type->getTypeName();
|
||||
}, $subjects[$field_name]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertEquals($relatable_type_names, $subjects);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getRelatableResourceTypes
|
||||
* @dataProvider getRelatableResourceTypesProvider
|
||||
*/
|
||||
public static function getRelatableResourceTypesProvider() {
|
||||
return [
|
||||
[
|
||||
'node--foo',
|
||||
[
|
||||
'field_ref_foo' => ['node--foo'],
|
||||
'field_ref_bar' => ['node--bar'],
|
||||
'field_ref_any' => ['node--foo', 'node--bar'],
|
||||
],
|
||||
],
|
||||
['node--bar', []],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getRelatableResourceTypesByField
|
||||
* @dataProvider getRelatableResourceTypesByFieldProvider
|
||||
*/
|
||||
public function testGetRelatableResourceTypesByField($entity_type_id, $bundle, $field): void {
|
||||
$resource_type = $this->resourceTypeRepository->get($entity_type_id, $bundle);
|
||||
$relatable_types = $resource_type->getRelatableResourceTypes();
|
||||
$this->assertSame(
|
||||
$relatable_types[$field],
|
||||
$resource_type->getRelatableResourceTypesByField($field)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides cases to test getRelatableTypesByField.
|
||||
*/
|
||||
public static function getRelatableResourceTypesByFieldProvider() {
|
||||
return [
|
||||
['node', 'foo', 'field_ref_foo'],
|
||||
['node', 'foo', 'field_ref_bar'],
|
||||
['node', 'foo', 'field_ref_any'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a graceful failure when a field can references a missing bundle.
|
||||
*
|
||||
* @covers \Drupal\jsonapi\ResourceType\ResourceTypeRepository::all
|
||||
* @covers \Drupal\jsonapi\ResourceType\ResourceTypeRepository::calculateRelatableResourceTypes
|
||||
* @covers \Drupal\jsonapi\ResourceType\ResourceTypeRepository::getRelatableResourceTypesFromFieldDefinition
|
||||
*
|
||||
* @link https://www.drupal.org/project/drupal/issues/2996114
|
||||
*/
|
||||
public function testGetRelatableResourceTypesFromFieldDefinition(): void {
|
||||
$field_config_storage = $this->container->get('entity_type.manager')->getStorage('field_config');
|
||||
|
||||
static::assertCount(0, $this->resourceTypeRepository->get('node', 'foo')->getRelatableResourceTypesByField('field_relationship'));
|
||||
$this->createEntityReferenceField('node', 'foo', 'field_ref_with_missing_bundle', 'Related entity', 'node', 'default', [
|
||||
'target_bundles' => ['missing_bundle'],
|
||||
]);
|
||||
$fields = $field_config_storage->loadByProperties(['field_name' => 'field_ref_with_missing_bundle']);
|
||||
static::assertSame(['missing_bundle'], $fields['node.foo.field_ref_with_missing_bundle']->getItemDefinition()->getSetting('handler_settings')['target_bundles']);
|
||||
$this->resourceTypeRepository->get('node', 'foo')->getRelatableResourceTypesByField('field_ref_with_missing_bundle');
|
||||
static::assertSame(['missing_bundle'], $fields['node.foo.field_ref_with_missing_bundle']->getItemDefinition()->getSetting('handler_settings')['target_bundles']);
|
||||
$arguments = [
|
||||
'@name' => 'field_ref_with_missing_bundle',
|
||||
'@target_entity_type_id' => 'node',
|
||||
'@target_bundle' => 'foo',
|
||||
'@entity_type_id' => 'node',
|
||||
'@bundle' => 'missing_bundle',
|
||||
];
|
||||
$logged = Database::getConnection()->select('watchdog')
|
||||
->fields('watchdog', ['variables'])
|
||||
->condition('type', 'jsonapi')
|
||||
->condition('message', 'The "@name" at "@target_entity_type_id:@target_bundle" references the "@entity_type_id:@bundle" entity type that does not exist.')
|
||||
->execute()
|
||||
->fetchField();
|
||||
$this->assertEquals(serialize($arguments), $logged);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Kernel\ResourceType;
|
||||
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\jsonapi\ResourceType\ResourceType;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\Tests\jsonapi\Kernel\JsonapiKernelTestBase;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\jsonapi\ResourceType\ResourceTypeRepository
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ResourceTypeRepositoryTest extends JsonapiKernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'file',
|
||||
'field',
|
||||
'node',
|
||||
'serialization',
|
||||
'system',
|
||||
'user',
|
||||
'jsonapi_test_resource_type_building',
|
||||
];
|
||||
|
||||
/**
|
||||
* The JSON:API resource type repository under test.
|
||||
*
|
||||
* @var \Drupal\jsonapi\ResourceType\ResourceTypeRepository
|
||||
*/
|
||||
protected $resourceTypeRepository;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
// Add the entity schemas.
|
||||
$this->installEntitySchema('node');
|
||||
$this->installEntitySchema('user');
|
||||
// Add the additional table schemas.
|
||||
$this->installSchema('node', ['node_access']);
|
||||
$this->installSchema('user', ['users_data']);
|
||||
NodeType::create([
|
||||
'type' => 'article',
|
||||
'name' => 'Article',
|
||||
])->save();
|
||||
NodeType::create([
|
||||
'type' => 'page',
|
||||
'name' => 'Page',
|
||||
])->save();
|
||||
NodeType::create([
|
||||
'type' => '42',
|
||||
'name' => '42',
|
||||
])->save();
|
||||
|
||||
$this->resourceTypeRepository = $this->container->get('jsonapi.resource_type.repository');
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::all
|
||||
*/
|
||||
public function testAll(): void {
|
||||
// Make sure that there are resources being created.
|
||||
$all = $this->resourceTypeRepository->all();
|
||||
$this->assertNotEmpty($all);
|
||||
array_walk($all, function (ResourceType $resource_type) {
|
||||
$this->assertNotEmpty($resource_type->getDeserializationTargetClass());
|
||||
$this->assertNotEmpty($resource_type->getEntityTypeId());
|
||||
$this->assertNotEmpty($resource_type->getTypeName());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::get
|
||||
* @dataProvider getProvider
|
||||
*/
|
||||
public function testGet($entity_type_id, $bundle, $entity_class): void {
|
||||
// Make sure that there are resources being created.
|
||||
$resource_type = $this->resourceTypeRepository->get($entity_type_id, $bundle);
|
||||
$this->assertInstanceOf(ResourceType::class, $resource_type);
|
||||
$this->assertSame($entity_class, $resource_type->getDeserializationTargetClass());
|
||||
$this->assertSame($entity_type_id, $resource_type->getEntityTypeId());
|
||||
$this->assertSame($bundle, $resource_type->getBundle());
|
||||
$this->assertSame($entity_type_id . '--' . $bundle, $resource_type->getTypeName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for testGet.
|
||||
*
|
||||
* @return array
|
||||
* The data for the test method.
|
||||
*/
|
||||
public static function getProvider() {
|
||||
return [
|
||||
['node', 'article', 'Drupal\node\Entity\Node'],
|
||||
['node', '42', 'Drupal\node\Entity\Node'],
|
||||
['node_type', 'node_type', 'Drupal\node\Entity\NodeType'],
|
||||
['menu', 'menu', 'Drupal\system\Entity\Menu'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the ResourceTypeRepository's cache does not become stale.
|
||||
*/
|
||||
public function testCaching(): void {
|
||||
$this->assertEmpty($this->resourceTypeRepository->get('node', 'article')->getRelatableResourceTypesByField('field_relationship'));
|
||||
$this->createEntityReferenceField('node', 'article', 'field_relationship', 'Related entity', 'node');
|
||||
$this->assertCount(3, $this->resourceTypeRepository->get('node', 'article')->getRelatableResourceTypesByField('field_relationship'));
|
||||
NodeType::create([
|
||||
'type' => 'camelids',
|
||||
'name' => 'Camelids',
|
||||
])->save();
|
||||
$this->assertCount(4, $this->resourceTypeRepository->get('node', 'article')->getRelatableResourceTypesByField('field_relationship'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that a naming conflict in mapping causes an exception to be thrown.
|
||||
*
|
||||
* @covers ::getFields
|
||||
* @dataProvider getFieldsProvider
|
||||
*/
|
||||
public function testMappingNameConflictCheck($field_name_list): void {
|
||||
$entity_type = \Drupal::entityTypeManager()->getDefinition('node');
|
||||
$bundle = 'article';
|
||||
$reflection_class = new \ReflectionClass($this->resourceTypeRepository);
|
||||
$reflection_method = $reflection_class->getMethod('getFields');
|
||||
|
||||
$this->expectException(\LogicException::class);
|
||||
$this->expectExceptionMessage("The generated alias '{$field_name_list[1]}' for field name '{$field_name_list[0]}' conflicts with an existing field. Report this in the JSON:API issue queue!");
|
||||
$reflection_method->invokeArgs($this->resourceTypeRepository, [$field_name_list, $entity_type, $bundle]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for testMappingNameConflictCheck.
|
||||
*
|
||||
* These field name lists are designed to trigger a naming conflict in the
|
||||
* mapping: the special-cased names "type" or "id", and the name
|
||||
* "{$entity_type_id}_type" or "{$entity_type_id}_id", respectively.
|
||||
*
|
||||
* @return array
|
||||
* The data for the test method.
|
||||
*/
|
||||
public static function getFieldsProvider() {
|
||||
return [
|
||||
[['type', 'node_type']],
|
||||
[['id', 'node_id']],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that resource types can be disabled by a build subscriber.
|
||||
*/
|
||||
public function testResourceTypeDisabling(): void {
|
||||
$this->assertFalse($this->resourceTypeRepository->getByTypeName('node--article')->isInternal());
|
||||
$this->assertFalse($this->resourceTypeRepository->getByTypeName('node--page')->isInternal());
|
||||
$this->assertFalse($this->resourceTypeRepository->getByTypeName('user--user')->isInternal());
|
||||
$disabled_resource_types = [
|
||||
'node--page',
|
||||
'user--user',
|
||||
];
|
||||
\Drupal::state()->set('jsonapi_test_resource_type_builder.disabled_resource_types', $disabled_resource_types);
|
||||
Cache::invalidateTags(['jsonapi_resource_types']);
|
||||
$this->assertFalse($this->resourceTypeRepository->getByTypeName('node--article')->isInternal());
|
||||
$this->assertTrue($this->resourceTypeRepository->getByTypeName('node--page')->isInternal());
|
||||
$this->assertTrue($this->resourceTypeRepository->getByTypeName('user--user')->isInternal());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that resource type fields can be aliased per resource type.
|
||||
*/
|
||||
public function testResourceTypeFieldAliasing(): void {
|
||||
$this->assertSame($this->resourceTypeRepository->getByTypeName('node--article')->getPublicName('uid'), 'uid');
|
||||
$this->assertSame($this->resourceTypeRepository->getByTypeName('node--page')->getPublicName('uid'), 'uid');
|
||||
$resource_type_field_aliases = [
|
||||
'node--article' => [
|
||||
'uid' => 'author',
|
||||
],
|
||||
'node--page' => [
|
||||
'uid' => 'owner',
|
||||
],
|
||||
];
|
||||
\Drupal::state()->set('jsonapi_test_resource_type_builder.resource_type_field_aliases', $resource_type_field_aliases);
|
||||
Cache::invalidateTags(['jsonapi_resource_types']);
|
||||
$this->assertSame($this->resourceTypeRepository->getByTypeName('node--article')->getPublicName('uid'), 'author');
|
||||
$this->assertSame($this->resourceTypeRepository->getByTypeName('node--page')->getPublicName('uid'), 'owner');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that resource type fields can be disabled per resource type.
|
||||
*/
|
||||
public function testResourceTypeFieldDisabling(): void {
|
||||
$this->assertTrue($this->resourceTypeRepository->getByTypeName('node--article')->isFieldEnabled('uid'));
|
||||
$this->assertTrue($this->resourceTypeRepository->getByTypeName('node--page')->isFieldEnabled('uid'));
|
||||
$disabled_resource_type_fields = [
|
||||
'node--article' => [
|
||||
'uid' => TRUE,
|
||||
],
|
||||
'node--page' => [
|
||||
'uid' => FALSE,
|
||||
],
|
||||
];
|
||||
\Drupal::state()->set('jsonapi_test_resource_type_builder.disabled_resource_type_fields', $disabled_resource_type_fields);
|
||||
Cache::invalidateTags(['jsonapi_resource_types']);
|
||||
$this->assertFalse($this->resourceTypeRepository->getByTypeName('node--article')->isFieldEnabled('uid'));
|
||||
$this->assertTrue($this->resourceTypeRepository->getByTypeName('node--page')->isFieldEnabled('uid'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that resource type fields can be re-enabled per resource type.
|
||||
*/
|
||||
public function testResourceTypeFieldEnabling(): void {
|
||||
$this->assertTrue($this->resourceTypeRepository->getByTypeName('node--article')->isFieldEnabled('uid'));
|
||||
$this->assertTrue($this->resourceTypeRepository->getByTypeName('node--page')->isFieldEnabled('uid'));
|
||||
$disabled_resource_type_fields = [
|
||||
'node--article' => [
|
||||
'uid' => TRUE,
|
||||
],
|
||||
'node--page' => [
|
||||
'uid' => TRUE,
|
||||
],
|
||||
];
|
||||
\Drupal::state()->set('jsonapi_test_resource_type_builder.disabled_resource_type_fields', $disabled_resource_type_fields);
|
||||
Cache::invalidateTags(['jsonapi_resource_types']);
|
||||
$this->assertFalse($this->resourceTypeRepository->getByTypeName('node--article')->isFieldEnabled('uid'));
|
||||
$this->assertFalse($this->resourceTypeRepository->getByTypeName('node--page')->isFieldEnabled('uid'));
|
||||
|
||||
$enabled_resource_type_fields = [
|
||||
'node--article' => [
|
||||
'uid' => TRUE,
|
||||
],
|
||||
'node--page' => [
|
||||
'uid' => TRUE,
|
||||
],
|
||||
];
|
||||
\Drupal::state()->set('jsonapi_test_resource_type_builder.enabled_resource_type_fields', $enabled_resource_type_fields);
|
||||
Cache::invalidateTags(['jsonapi_resource_types']);
|
||||
$this->assertTrue($this->resourceTypeRepository->getByTypeName('node--article')->isFieldEnabled('uid'));
|
||||
$this->assertTrue($this->resourceTypeRepository->getByTypeName('node--page')->isFieldEnabled('uid'));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that resource types can be renamed.
|
||||
*/
|
||||
public function testResourceTypeRenaming(): void {
|
||||
\Drupal::state()->set('jsonapi_test_resource_type_builder.renamed_resource_types', [
|
||||
'node--article' => 'articles',
|
||||
'node--page' => 'pages',
|
||||
]);
|
||||
Cache::invalidateTags(['jsonapi_resource_types']);
|
||||
$this->assertNull($this->resourceTypeRepository->getByTypeName('node--article'));
|
||||
$this->assertInstanceOf(ResourceType::class, $this->resourceTypeRepository->getByTypeName('articles'));
|
||||
$this->assertNull($this->resourceTypeRepository->getByTypeName('node--page'));
|
||||
$this->assertInstanceOf(ResourceType::class, $this->resourceTypeRepository->getByTypeName('pages'));
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Kernel\Revisions;
|
||||
|
||||
use Drupal\Core\Http\Exception\CacheableBadRequestHttpException;
|
||||
use Drupal\Core\Http\Exception\CacheableNotFoundHttpException;
|
||||
use Drupal\jsonapi\Revisions\VersionById;
|
||||
use Drupal\jsonapi\Revisions\VersionByRel;
|
||||
use Drupal\jsonapi\Revisions\VersionNegotiator;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\Tests\jsonapi\Kernel\JsonapiKernelTestBase;
|
||||
use Drupal\user\Entity\User;
|
||||
|
||||
/**
|
||||
* The test class for version negotiators.
|
||||
*
|
||||
* @coversDefaultClass \Drupal\jsonapi\Revisions\VersionNegotiator
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class VersionNegotiatorTest extends JsonapiKernelTestBase {
|
||||
|
||||
/**
|
||||
* The user.
|
||||
*
|
||||
* @var \Drupal\user\Entity\User
|
||||
*/
|
||||
protected $user;
|
||||
|
||||
/**
|
||||
* The node.
|
||||
*
|
||||
* @var \Drupal\node\Entity\Node
|
||||
*/
|
||||
protected $node;
|
||||
|
||||
/**
|
||||
* The previous revision ID of $node.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $nodePreviousRevisionId;
|
||||
|
||||
/**
|
||||
* The version negotiator service.
|
||||
*
|
||||
* @var \Drupal\jsonapi\Revisions\VersionNegotiator
|
||||
*/
|
||||
protected $versionNegotiator;
|
||||
|
||||
/**
|
||||
* The other node.
|
||||
*
|
||||
* @var \Drupal\node\Entity\Node
|
||||
*/
|
||||
protected $node2;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'file',
|
||||
'node',
|
||||
'field',
|
||||
'jsonapi',
|
||||
'serialization',
|
||||
'system',
|
||||
'user',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
// Add the entity schemas.
|
||||
$this->installEntitySchema('node');
|
||||
$this->installEntitySchema('user');
|
||||
// Add the additional table schemas.
|
||||
$this->installSchema('node', ['node_access']);
|
||||
$this->installSchema('user', ['users_data']);
|
||||
$type = NodeType::create([
|
||||
'type' => 'dummy',
|
||||
'name' => 'Dummy',
|
||||
'new_revision' => TRUE,
|
||||
]);
|
||||
$type->save();
|
||||
|
||||
$this->user = User::create([
|
||||
'name' => 'user1',
|
||||
'mail' => 'user@localhost',
|
||||
'status' => 1,
|
||||
]);
|
||||
$this->user->save();
|
||||
|
||||
$this->node = Node::create([
|
||||
'title' => 'dummy_title',
|
||||
'type' => 'dummy',
|
||||
'uid' => $this->user->id(),
|
||||
]);
|
||||
$this->node->save();
|
||||
|
||||
$this->nodePreviousRevisionId = $this->node->getRevisionId();
|
||||
|
||||
$this->node->setNewRevision();
|
||||
$this->node->setTitle('revised_dummy_title');
|
||||
$this->node->save();
|
||||
|
||||
$this->node2 = Node::create([
|
||||
'type' => 'dummy',
|
||||
'title' => 'Another test node',
|
||||
'uid' => $this->user->id(),
|
||||
]);
|
||||
$this->node2->save();
|
||||
|
||||
$entity_type_manager = \Drupal::entityTypeManager();
|
||||
$version_negotiator = new VersionNegotiator();
|
||||
$version_negotiator->addVersionNegotiator(new VersionById($entity_type_manager), 'id');
|
||||
$version_negotiator->addVersionNegotiator(new VersionByRel($entity_type_manager), 'rel');
|
||||
$this->versionNegotiator = $version_negotiator;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \Drupal\jsonapi\Revisions\VersionById::getRevision
|
||||
*/
|
||||
public function testOldRevision(): void {
|
||||
$revision = $this->versionNegotiator->getRevision($this->node, 'id:' . $this->nodePreviousRevisionId);
|
||||
$this->assertEquals($this->node->id(), $revision->id());
|
||||
$this->assertEquals($this->nodePreviousRevisionId, $revision->getRevisionId());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \Drupal\jsonapi\Revisions\VersionById::getRevision
|
||||
*/
|
||||
public function testInvalidRevisionId(): void {
|
||||
$this->expectException(CacheableNotFoundHttpException::class);
|
||||
$this->expectExceptionMessage(sprintf('The requested version, identified by `id:%s`, could not be found.', $this->node2->getRevisionId()));
|
||||
$this->versionNegotiator->getRevision($this->node, 'id:' . $this->node2->getRevisionId());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \Drupal\jsonapi\Revisions\VersionByRel::getRevision
|
||||
*/
|
||||
public function testLatestVersion(): void {
|
||||
$revision = $this->versionNegotiator->getRevision($this->node, 'rel:' . VersionByRel::LATEST_VERSION);
|
||||
$this->assertEquals($this->node->id(), $revision->id());
|
||||
$this->assertEquals($this->node->getRevisionId(), $revision->getRevisionId());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \Drupal\jsonapi\Revisions\VersionByRel::getRevision
|
||||
*/
|
||||
public function testCurrentVersion(): void {
|
||||
$revision = $this->versionNegotiator->getRevision($this->node, 'rel:' . VersionByRel::WORKING_COPY);
|
||||
$this->assertEquals($this->node->id(), $revision->id());
|
||||
$this->assertEquals($this->node->id(), $revision->id());
|
||||
$this->assertEquals($this->node->getRevisionId(), $revision->getRevisionId());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \Drupal\jsonapi\Revisions\VersionByRel::getRevision
|
||||
*/
|
||||
public function testInvalidRevisionRel(): void {
|
||||
$this->expectException(CacheableBadRequestHttpException::class);
|
||||
$this->expectExceptionMessage('An invalid resource version identifier, `rel:erroneous-revision-name`, was provided.');
|
||||
$this->versionNegotiator->getRevision($this->node, 'rel:erroneous-revision-name');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Kernel\Serializer;
|
||||
|
||||
use Drupal\Core\Render\Markup;
|
||||
use Drupal\jsonapi\JsonApiResource\ResourceObject;
|
||||
use Drupal\jsonapi\Normalizer\Value\CacheableNormalization;
|
||||
use Drupal\jsonapi_test_data_type\TraversableObject;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\Tests\jsonapi\Kernel\JsonapiKernelTestBase;
|
||||
use Drupal\user\Entity\User;
|
||||
|
||||
/**
|
||||
* Tests the JSON:API serializer.
|
||||
*
|
||||
* @coversClass \Drupal\jsonapi\Serializer\Serializer
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class SerializerTest extends JsonapiKernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'file',
|
||||
'serialization',
|
||||
'system',
|
||||
'node',
|
||||
'user',
|
||||
'field',
|
||||
'text',
|
||||
'filter',
|
||||
'jsonapi_test_data_type',
|
||||
];
|
||||
|
||||
/**
|
||||
* An entity for testing.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityInterface
|
||||
*/
|
||||
protected $node;
|
||||
|
||||
/**
|
||||
* A resource type for testing.
|
||||
*
|
||||
* @var \Drupal\jsonapi\ResourceType\ResourceType
|
||||
*/
|
||||
protected $resourceType;
|
||||
|
||||
/**
|
||||
* The subject under test.
|
||||
*
|
||||
* @var \Drupal\jsonapi\Serializer\Serializer
|
||||
*/
|
||||
protected $sut;
|
||||
|
||||
/**
|
||||
* A user.
|
||||
*
|
||||
* @var \Drupal\user\Entity\User
|
||||
*/
|
||||
protected User $user;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
// Add the entity schemas.
|
||||
$this->installEntitySchema('node');
|
||||
$this->installEntitySchema('user');
|
||||
// Add the additional table schemas.
|
||||
$this->installSchema('node', ['node_access']);
|
||||
$this->installSchema('user', ['users_data']);
|
||||
$this->user = User::create([
|
||||
'name' => $this->randomString(),
|
||||
'status' => 1,
|
||||
]);
|
||||
$this->user->save();
|
||||
NodeType::create([
|
||||
'type' => 'foo',
|
||||
'name' => 'Foo',
|
||||
])->save();
|
||||
$this->createTextField('node', 'foo', 'field_text', 'Text');
|
||||
$this->node = Node::create([
|
||||
'title' => 'Test Node',
|
||||
'type' => 'foo',
|
||||
'field_text' => [
|
||||
'value' => 'This is some text.',
|
||||
'format' => 'text_plain',
|
||||
],
|
||||
'uid' => $this->user->id(),
|
||||
]);
|
||||
$this->node->save();
|
||||
$this->container->setAlias('sut', 'jsonapi.serializer');
|
||||
$this->resourceType = $this->container->get('jsonapi.resource_type.repository')->get($this->node->getEntityTypeId(), $this->node->bundle());
|
||||
$this->sut = $this->container->get('sut');
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \Drupal\jsonapi\Serializer\Serializer::normalize
|
||||
*/
|
||||
public function testFallbackNormalizer(): void {
|
||||
$context = [
|
||||
'account' => $this->user,
|
||||
'resource_object' => ResourceObject::createFromEntity($this->resourceType, $this->node),
|
||||
];
|
||||
|
||||
$value = $this->sut->normalize($this->node->field_text, 'api_json', $context);
|
||||
$this->assertInstanceOf(CacheableNormalization::class, $value);
|
||||
|
||||
$nested_field = [
|
||||
$this->node->field_text,
|
||||
];
|
||||
|
||||
// When an object implements \IteratorAggregate and has corresponding
|
||||
// fallback normalizer, it should be normalized by fallback normalizer.
|
||||
$traversableObject = new TraversableObject();
|
||||
$value = $this->sut->normalize($traversableObject, 'api_json', $context);
|
||||
$this->assertEquals($traversableObject->property, $value);
|
||||
|
||||
// When wrapped in an array, we should still be using the JSON:API
|
||||
// serializer.
|
||||
$value = $this->sut->normalize($nested_field, 'api_json', $context);
|
||||
$this->assertInstanceOf(CacheableNormalization::class, $value[0]);
|
||||
|
||||
// Continue to use the fallback normalizer when we need it.
|
||||
$data = Markup::create('<h2>Test Markup</h2>');
|
||||
$value = $this->sut->normalize($data, 'api_json', $context);
|
||||
|
||||
$this->assertEquals('<h2>Test Markup</h2>', $value);
|
||||
}
|
||||
|
||||
}
|
||||
107
web/core/modules/jsonapi/tests/src/Kernel/TestCoverageTest.php
Normal file
107
web/core/modules/jsonapi/tests/src/Kernel/TestCoverageTest.php
Normal file
@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Kernel;
|
||||
|
||||
use Drupal\Core\Config\Entity\ConfigEntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Extension\ExtensionLifecycle;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\Tests\jsonapi\Functional\ConfigEntityResourceTestBase;
|
||||
|
||||
/**
|
||||
* Checks that all core content/config entity types have JSON:API test coverage.
|
||||
*
|
||||
* @group jsonapi
|
||||
* @group #slow
|
||||
*/
|
||||
class TestCoverageTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* Entity definitions array.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $definitions;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['system', 'user'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$all_modules = \Drupal::service('extension.list.module')->getList();
|
||||
$stable_core_modules = array_filter($all_modules, function ($module) {
|
||||
// Filter out contrib, hidden, testing, experimental, and deprecated
|
||||
// modules. We also don't need to enable modules that are already enabled.
|
||||
return $module->origin === 'core'
|
||||
&& empty($module->info['hidden'])
|
||||
&& $module->status == FALSE
|
||||
&& $module->info['package'] !== 'Testing'
|
||||
&& $module->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] !== ExtensionLifecycle::EXPERIMENTAL
|
||||
&& $module->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] !== ExtensionLifecycle::DEPRECATED;
|
||||
});
|
||||
|
||||
$this->container->get('module_installer')->install(array_keys($stable_core_modules));
|
||||
|
||||
$this->definitions = $this->container->get('entity_type.manager')->getDefinitions();
|
||||
|
||||
// Entity types marked as "internal" are not exposed by JSON:API and hence
|
||||
// also don't need test coverage.
|
||||
$this->definitions = array_filter($this->definitions, function (EntityTypeInterface $entity_type) {
|
||||
return !$entity_type->isInternal();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that all core entity types have JSON:API test coverage.
|
||||
*/
|
||||
public function testEntityTypeRestTestCoverage(): void {
|
||||
$problems = [];
|
||||
foreach ($this->definitions as $entity_type_id => $info) {
|
||||
$class_name_full = $info->getClass();
|
||||
$parts = explode('\\', $class_name_full);
|
||||
$class_name = end($parts);
|
||||
$module_name = $parts[1];
|
||||
|
||||
$possible_paths = [
|
||||
'Drupal\Tests\jsonapi\Functional\CLASSTest',
|
||||
'\Drupal\Tests\\' . $module_name . '\Functional\Jsonapi\CLASSTest',
|
||||
// For entities defined in the system module with Jsonapi tests in
|
||||
// another module.
|
||||
'\Drupal\Tests\\' . $info->id() . '\Functional\Jsonapi\CLASSTest',
|
||||
];
|
||||
foreach ($possible_paths as $path) {
|
||||
$missing_tests = [];
|
||||
$class = str_replace('CLASS', $class_name, $path);
|
||||
if (class_exists($class)) {
|
||||
break;
|
||||
}
|
||||
$missing_tests[] = $class;
|
||||
}
|
||||
if (!empty($missing_tests)) {
|
||||
$missing_tests_list = implode(', ', $missing_tests);
|
||||
$problems[] = "$entity_type_id: $class_name ($class_name_full) (expected tests: $missing_tests_list)";
|
||||
}
|
||||
else {
|
||||
$config_entity = is_subclass_of($class_name_full, ConfigEntityInterface::class);
|
||||
$config_test = is_subclass_of($class, ConfigEntityResourceTestBase::class);
|
||||
if ($config_entity && !$config_test) {
|
||||
$problems[] = "$entity_type_id: $class_name is a config entity, but the test is for content entities.";
|
||||
}
|
||||
elseif (!$config_entity && $config_test) {
|
||||
$problems[] = "$entity_type_id: $class_name is a content entity, but the test is for config entities.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $problems);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Traits;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Core\Entity\EntityPublishedInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\entity_test\Entity\EntityTest;
|
||||
use Drupal\entity_test\EntityTestHelper;
|
||||
use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
|
||||
use Drupal\Tests\jsonapi\Functional\ResourceTestBase;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
|
||||
/**
|
||||
* Provides common filter access control tests.
|
||||
*/
|
||||
trait CommonCollectionFilterAccessTestPatternsTrait {
|
||||
|
||||
use EntityReferenceFieldCreationTrait;
|
||||
|
||||
/**
|
||||
* Implements ::testCollectionFilterAccess() for pure permission-based access.
|
||||
*
|
||||
* @param string $label_field_name
|
||||
* The entity type's label field name.
|
||||
* @param string $view_permission
|
||||
* The entity type's permission that grants 'view' access.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityInterface
|
||||
* The referencing entity.
|
||||
*/
|
||||
public function doTestCollectionFilterAccessBasedOnPermissions($label_field_name, $view_permission) {
|
||||
assert($this instanceof ResourceTestBase);
|
||||
|
||||
// Set up data model.
|
||||
$this->assertTrue($this->container->get('module_installer')->install(['entity_test'], TRUE), 'Installed modules.');
|
||||
EntityTestHelper::createBundle('bar', NULL, 'entity_test');
|
||||
$this->createEntityReferenceField(
|
||||
'entity_test',
|
||||
'bar',
|
||||
'spotlight',
|
||||
NULL,
|
||||
static::$entityTypeId,
|
||||
'default',
|
||||
[
|
||||
'target_bundles' => [
|
||||
$this->entity->bundle() => $this->entity->bundle(),
|
||||
],
|
||||
]
|
||||
);
|
||||
$this->rebuildAll();
|
||||
$this->grantPermissionsToTestedRole(['view test entity']);
|
||||
|
||||
// Create data.
|
||||
$referencing_entity = EntityTest::create([
|
||||
'name' => 'Camelids',
|
||||
'type' => 'bar',
|
||||
'spotlight' => [
|
||||
'target_id' => $this->entity->id(),
|
||||
],
|
||||
]);
|
||||
$referencing_entity->save();
|
||||
|
||||
// Test.
|
||||
$collection_url = Url::fromRoute('jsonapi.entity_test--bar.collection');
|
||||
// Specifying a delta exercises TemporaryQueryGuard more thoroughly.
|
||||
$filter_path = "spotlight.0.$label_field_name";
|
||||
$collection_filter_url = $collection_url->setOption('query', ["filter[$filter_path]" => $this->entity->label()]);
|
||||
$request_options = [];
|
||||
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
|
||||
$request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions());
|
||||
if ($view_permission !== NULL) {
|
||||
// ?filter[spotlight.LABEL]: 0 results.
|
||||
$response = $this->request('GET', $collection_filter_url, $request_options);
|
||||
$doc = Json::decode((string) $response->getBody());
|
||||
$this->assertCount(0, $doc['data']);
|
||||
// Grant "view" permission.
|
||||
$this->grantPermissionsToTestedRole([$view_permission]);
|
||||
}
|
||||
// ?filter[spotlight.LABEL]: 1 result.
|
||||
$response = $this->request('GET', $collection_filter_url, $request_options);
|
||||
$doc = Json::decode((string) $response->getBody());
|
||||
$this->assertCount(1, $doc['data']);
|
||||
$this->assertSame($referencing_entity->uuid(), $doc['data'][0]['id']);
|
||||
|
||||
// ?filter[spotlight.LABEL]: 1 result.
|
||||
$response = $this->request('GET', $collection_filter_url, $request_options);
|
||||
$doc = Json::decode((string) $response->getBody());
|
||||
$this->assertCount(1, $doc['data']);
|
||||
$this->assertSame($referencing_entity->uuid(), $doc['data'][0]['id']);
|
||||
|
||||
// Install the jsonapi_test_field_filter_access module, which contains a
|
||||
// hook_jsonapi_entity_field_filter_access() implementation that forbids
|
||||
// access to the spotlight field if the 'filter by spotlight field'
|
||||
// permission is not granted.
|
||||
$this->assertTrue($this->container->get('module_installer')->install(['jsonapi_test_field_filter_access'], TRUE), 'Installed modules.');
|
||||
$this->rebuildAll();
|
||||
|
||||
// Ensure that a 403 response is generated for attempting to filter by a
|
||||
// field that is forbidden by an implementation of
|
||||
// hook_jsonapi_entity_field_filter_access() .
|
||||
$response = $this->request('GET', $collection_filter_url, $request_options);
|
||||
$message = "The current user is not authorized to filter by the `spotlight` field, given in the path `spotlight`.";
|
||||
$expected_cache_tags = ['4xx-response', 'http_response'];
|
||||
$expected_cache_contexts = [
|
||||
'url.query_args',
|
||||
'url.site',
|
||||
'user.permissions',
|
||||
];
|
||||
$this->assertResourceErrorResponse(403, $message, $collection_filter_url, $response, FALSE, $expected_cache_tags, $expected_cache_contexts, NULL, 'MISS');
|
||||
// And ensure the it is allowed when the proper permission is granted.
|
||||
$this->grantPermissionsToTestedRole(['filter by spotlight field']);
|
||||
$response = $this->request('GET', $collection_filter_url, $request_options);
|
||||
$doc = Json::decode((string) $response->getBody());
|
||||
$this->assertCount(1, $doc['data']);
|
||||
$this->assertSame($referencing_entity->uuid(), $doc['data'][0]['id']);
|
||||
$this->revokePermissionsFromTestedRole(['filter by spotlight field']);
|
||||
|
||||
$this->assertTrue($this->container->get('module_installer')->uninstall(['jsonapi_test_field_filter_access'], TRUE), 'Uninstalled modules.');
|
||||
|
||||
return $referencing_entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements ::testCollectionFilterAccess() for permission + status access.
|
||||
*
|
||||
* @param string $label_field_name
|
||||
* The entity type's label field name.
|
||||
* @param string $view_permission
|
||||
* The entity type's permission that grants 'view' access (for published
|
||||
* entities of this type).
|
||||
* @param string $admin_permission
|
||||
* The entity type's permission that grants 'view' access (for unpublished
|
||||
* entities of this type).
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityInterface
|
||||
* The referencing entity.
|
||||
*/
|
||||
public function doTestCollectionFilterAccessForPublishableEntities($label_field_name, $view_permission, $admin_permission) {
|
||||
assert($this->entity instanceof EntityPublishedInterface);
|
||||
$this->assertTrue($this->entity->isPublished());
|
||||
|
||||
$referencing_entity = $this->doTestCollectionFilterAccessBasedOnPermissions($label_field_name, $view_permission);
|
||||
|
||||
$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());
|
||||
|
||||
// Unpublish.
|
||||
$this->entity->setUnpublished()->save();
|
||||
// ?filter[spotlight.LABEL]: no result because the test entity is
|
||||
// unpublished. This proves that appropriate cache tags are bubbled.
|
||||
$response = $this->request('GET', $collection_filter_url, $request_options);
|
||||
$doc = Json::decode((string) $response->getBody());
|
||||
$this->assertCount(0, $doc['data']);
|
||||
// Grant admin permission.
|
||||
$this->grantPermissionsToTestedRole([$admin_permission]);
|
||||
// ?filter[spotlight.LABEL]: 1 result despite the test entity being
|
||||
// unpublished, thanks to the admin permission. This proves that the
|
||||
// appropriate cache contexts are bubbled.
|
||||
$response = $this->request('GET', $collection_filter_url, $request_options);
|
||||
$doc = Json::decode((string) $response->getBody());
|
||||
$this->assertCount(1, $doc['data']);
|
||||
$this->assertSame($referencing_entity->uuid(), $doc['data'][0]['id']);
|
||||
|
||||
return $referencing_entity;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Traits;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Test trait for retrieving the JSON:API document from a response.
|
||||
*/
|
||||
trait GetDocumentFromResponseTrait {
|
||||
|
||||
/**
|
||||
* Retrieve document from response, with basic validation.
|
||||
*
|
||||
* @param \Psr\Http\Message\ResponseInterface $response
|
||||
* Response to extract JSON:API document from.
|
||||
* @param bool $validate
|
||||
* Determines whether the data is validated or not. Defaults to TRUE.
|
||||
*
|
||||
* @return ?array
|
||||
* JSON:API document extracted from the response, or NULL.
|
||||
*
|
||||
* @throws \PHPUnit\Framework\AssertionFailedError
|
||||
* Thrown when the document does not pass basic validation against the spec.
|
||||
*/
|
||||
protected function getDocumentFromResponse(ResponseInterface $response, bool $validate = TRUE): ?array {
|
||||
assert($this instanceof TestCase);
|
||||
|
||||
$document = Json::decode((string) $response->getBody());
|
||||
|
||||
if (isset($document['data']) && isset($document['errors'])) {
|
||||
$this->fail('Document contains both data and errors members; only one is allowed.');
|
||||
}
|
||||
|
||||
if ($validate === TRUE && !isset($document['data'])) {
|
||||
if (isset($document['errors'])) {
|
||||
$errors = [];
|
||||
foreach ($document['errors'] as $error) {
|
||||
$errors[] = $error['title'] . ' (' . $error['status'] . '): ' . $error['detail'];
|
||||
}
|
||||
$this->fail('Missing expected data member in document. Error(s): ' . PHP_EOL . ' ' . implode(' ' . PHP_EOL, $errors));
|
||||
}
|
||||
$this->fail('Missing both data and errors members in document; either is required. Response body: ' . PHP_EOL . ' ' . $response->getBody());
|
||||
}
|
||||
return $document;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Traits;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\jsonapi\Normalizer\Value\CacheableNormalization;
|
||||
use Drupal\Tests\serialization\Traits\JsonSchemaTestTrait;
|
||||
use JsonSchema\Constraints\Factory;
|
||||
use JsonSchema\Uri\UriRetriever;
|
||||
use JsonSchema\Validator;
|
||||
|
||||
/**
|
||||
* Support methods for testing JSON API schema.
|
||||
*/
|
||||
trait JsonApiJsonSchemaTestTrait {
|
||||
|
||||
use JsonSchemaTestTrait {
|
||||
getNormalizationForValue as parentGetNormalizationForValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getJsonSchemaTestNormalizationFormat(): ?string {
|
||||
return 'api_json';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getValidator(): Validator {
|
||||
$uriRetriever = new UriRetriever();
|
||||
$uriRetriever->setTranslation(
|
||||
'|^' . JsonApiSpec::SUPPORTED_SPECIFICATION_JSON_SCHEMA . '#?|',
|
||||
sprintf('file://%s/schema.json', realpath(__DIR__ . '/../../..'))
|
||||
);
|
||||
return new Validator(new Factory(
|
||||
uriRetriever: $uriRetriever,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getNormalizationForValue(mixed $value): mixed {
|
||||
$normalization = $this->parentGetNormalizationForValue($value);
|
||||
if ($normalization instanceof CacheableNormalization) {
|
||||
return $normalization->getNormalization();
|
||||
}
|
||||
return $normalization;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Unit\EventSubscriber;
|
||||
|
||||
use Drupal\jsonapi\EventSubscriber\ResourceResponseValidator;
|
||||
use Drupal\jsonapi\ResourceType\ResourceType;
|
||||
use Drupal\jsonapi\Routing\Routes;
|
||||
use Drupal\Core\Extension\Extension;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Drupal\rest\ResourceResponse;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Drupal\Core\Routing\RouteObjectInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\jsonapi\EventSubscriber\ResourceResponseValidator
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ResourceResponseValidatorTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* The subscriber under test.
|
||||
*
|
||||
* @var \Drupal\jsonapi\EventSubscriber\ResourceResponseSubscriber
|
||||
*/
|
||||
protected $subscriber;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
// Check that the validation class is available.
|
||||
if (!class_exists("\\JsonSchema\\Validator")) {
|
||||
$this->fail('The JSON Schema validator is missing. You can install it with `composer require justinrainbow/json-schema`.');
|
||||
}
|
||||
|
||||
$module_handler = $this->prophesize(ModuleHandlerInterface::class);
|
||||
$module = $this->prophesize(Extension::class);
|
||||
$module_path = dirname(__DIR__, 4);
|
||||
$module->getPath()->willReturn($module_path);
|
||||
$module_handler->getModule('jsonapi')->willReturn($module->reveal());
|
||||
$subscriber = new ResourceResponseValidator(
|
||||
$this->prophesize(LoggerInterface::class)->reveal(),
|
||||
$module_handler->reveal(),
|
||||
''
|
||||
);
|
||||
$subscriber->setValidator();
|
||||
$this->subscriber = $subscriber;
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::validateResponse
|
||||
* @dataProvider validateResponseProvider
|
||||
*/
|
||||
public function testValidateResponse($request, $response, $expected, $description): void {
|
||||
// Expose protected ResourceResponseSubscriber::validateResponse() method.
|
||||
$object = new \ReflectionObject($this->subscriber);
|
||||
$method = $object->getMethod('validateResponse');
|
||||
|
||||
$this->assertSame($expected, $method->invoke($this->subscriber, $response, $request), $description);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides test cases for testValidateResponse.
|
||||
*
|
||||
* @return array
|
||||
* An array of test cases.
|
||||
*/
|
||||
public static function validateResponseProvider() {
|
||||
$defaults = [
|
||||
'route_name' => 'jsonapi.node--article.individual',
|
||||
'resource_type' => new ResourceType('node', 'article', NULL),
|
||||
];
|
||||
|
||||
$test_data = [
|
||||
// Test validation success.
|
||||
[
|
||||
'json' => <<<'EOD'
|
||||
{
|
||||
"data": {
|
||||
"type": "node--article",
|
||||
"id": "4f342419-e668-4b76-9f87-7ce20c436169",
|
||||
"attributes": {
|
||||
"nid": "1",
|
||||
"uuid": "4f342419-e668-4b76-9f87-7ce20c436169"
|
||||
}
|
||||
}
|
||||
}
|
||||
EOD
|
||||
,
|
||||
'expected' => TRUE,
|
||||
'description' => 'Response validation flagged a valid response.',
|
||||
],
|
||||
// Test validation failure: no "type" in "data".
|
||||
[
|
||||
'json' => <<<'EOD'
|
||||
{
|
||||
"data": {
|
||||
"id": "4f342419-e668-4b76-9f87-7ce20c436169",
|
||||
"attributes": {
|
||||
"nid": "1",
|
||||
"uuid": "4f342419-e668-4b76-9f87-7ce20c436169"
|
||||
}
|
||||
}
|
||||
}
|
||||
EOD
|
||||
,
|
||||
'expected' => FALSE,
|
||||
'description' => 'Response validation failed to flag an invalid response.',
|
||||
],
|
||||
// Test validation failure: "errors" at the root level.
|
||||
[
|
||||
'json' => <<<'EOD'
|
||||
{
|
||||
"data": {
|
||||
"type": "node--article",
|
||||
"id": "4f342419-e668-4b76-9f87-7ce20c436169",
|
||||
"attributes": {
|
||||
"nid": "1",
|
||||
"uuid": "4f342419-e668-4b76-9f87-7ce20c436169"
|
||||
}
|
||||
},
|
||||
"errors": [{}]
|
||||
}
|
||||
EOD
|
||||
,
|
||||
'expected' => FALSE,
|
||||
'description' => 'Response validation failed to flag an invalid response.',
|
||||
],
|
||||
// Test validation of an empty response passes.
|
||||
[
|
||||
'json' => NULL,
|
||||
'expected' => TRUE,
|
||||
'description' => 'Response validation flagged a valid empty response.',
|
||||
],
|
||||
// Test validation fails on empty object.
|
||||
[
|
||||
'json' => '{}',
|
||||
'expected' => FALSE,
|
||||
'description' => 'Response validation flags empty array as invalid.',
|
||||
],
|
||||
];
|
||||
|
||||
$test_cases = array_map(function ($input) use ($defaults) {
|
||||
[$json, $expected, $description, $route_name, $resource_type] = array_values($input + $defaults);
|
||||
return [
|
||||
static::createRequest($route_name, $resource_type),
|
||||
static::createResponse($json),
|
||||
$expected,
|
||||
$description,
|
||||
];
|
||||
}, $test_data);
|
||||
|
||||
return $test_cases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to create a request object.
|
||||
*
|
||||
* @param string $route_name
|
||||
* The route name with which to construct a request.
|
||||
* @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
|
||||
* The resource type for the requested route.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\Request
|
||||
* The mock request object.
|
||||
*/
|
||||
protected static function createRequest(string $route_name, ResourceType $resource_type): Request {
|
||||
$request = new Request();
|
||||
$request->attributes->set(RouteObjectInterface::ROUTE_NAME, $route_name);
|
||||
$request->attributes->set(Routes::RESOURCE_TYPE_KEY, $resource_type);
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to create a resource response from arbitrary JSON.
|
||||
*
|
||||
* @param string|null $json
|
||||
* The JSON with which to create a mock response.
|
||||
*
|
||||
* @return \Drupal\rest\ResourceResponse
|
||||
* The mock response object.
|
||||
*/
|
||||
protected static function createResponse(?string $json = NULL): ResourceResponse {
|
||||
$response = new ResourceResponse();
|
||||
if ($json) {
|
||||
$response->setContent($json);
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Unit\JsonApiResource;
|
||||
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\DependencyInjection\ContainerBuilder;
|
||||
use Drupal\Core\GeneratedUrl;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\Core\Utility\UnroutedUrlAssemblerInterface;
|
||||
use Drupal\jsonapi\JsonApiResource\Link;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\jsonapi\JsonApiResource\Link
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class LinkTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* @covers ::compare
|
||||
* @dataProvider linkComparisonProvider
|
||||
*/
|
||||
public function testLinkComparison(array $a, array $b, bool $expected): void {
|
||||
$this->mockUrlAssembler();
|
||||
|
||||
$link_a = new Link(new CacheableMetadata(), Url::fromUri($a[0]), $a[1], $a[2] ?? []);
|
||||
$link_b = new Link(new CacheableMetadata(), Url::fromUri($b[0]), $b[1], $b[2] ?? []);
|
||||
|
||||
$actual = Link::compare($link_a, $link_b);
|
||||
$this->assertSame($expected, $actual === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides test data for link comparison.
|
||||
*/
|
||||
public static function linkComparisonProvider(): \Generator {
|
||||
yield 'same href and same link relation type' => [
|
||||
['https://jsonapi.org/foo', 'self'],
|
||||
['https://jsonapi.org/foo', 'self'],
|
||||
TRUE,
|
||||
];
|
||||
yield 'different href and same link relation type' => [
|
||||
['https://jsonapi.org/foo', 'self'],
|
||||
['https://jsonapi.org/bar', 'self'],
|
||||
FALSE,
|
||||
];
|
||||
yield 'same href and different link relation type' => [
|
||||
['https://jsonapi.org/foo', 'self'],
|
||||
['https://jsonapi.org/foo', 'related'],
|
||||
FALSE,
|
||||
];
|
||||
yield 'same href and same link relation type and empty target attributes' => [
|
||||
['https://jsonapi.org/foo', 'self', []],
|
||||
['https://jsonapi.org/foo', 'self', []],
|
||||
TRUE,
|
||||
];
|
||||
yield 'same href and same link relation type and same target attributes' => [
|
||||
['https://jsonapi.org/foo', 'self', ['anchor' => 'https://jsonapi.org']],
|
||||
['https://jsonapi.org/foo', 'self', ['anchor' => 'https://jsonapi.org']],
|
||||
TRUE,
|
||||
];
|
||||
// These links are not considered equivalent because it would while the
|
||||
// `href` remains the same, the anchor changes the context of the link.
|
||||
yield 'same href and same link relation type and different target attributes' => [
|
||||
['https://jsonapi.org/boy', 'self', ['title' => 'sue']],
|
||||
['https://jsonapi.org/boy', 'self', ['anchor' => '/sob', 'title' => 'pa']],
|
||||
FALSE,
|
||||
];
|
||||
yield 'same href and same link relation type and same nested target attributes' => [
|
||||
['https://jsonapi.org/foo', 'self', ['data' => ['foo' => 'bar']]],
|
||||
['https://jsonapi.org/foo', 'self', ['data' => ['foo' => 'bar']]],
|
||||
TRUE,
|
||||
];
|
||||
yield 'same href and same link relation type and different nested target attributes' => [
|
||||
['https://jsonapi.org/foo', 'self', ['data' => ['foo' => 'bar']]],
|
||||
['https://jsonapi.org/foo', 'self', ['data' => ['foo' => 'baz']]],
|
||||
FALSE,
|
||||
];
|
||||
// These links are not considered equivalent because it would be unclear
|
||||
// which title corresponds to which link relation type.
|
||||
yield 'same href and different link relation types and different target attributes' => [
|
||||
['https://jsonapi.org/boy', 'self', ['title' => 'A boy named Sue']],
|
||||
['https://jsonapi.org/boy', 'edit', ['title' => 'Change name to Bill or George']],
|
||||
FALSE,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::merge
|
||||
* @dataProvider linkMergeProvider
|
||||
*/
|
||||
public function testLinkMerge(array $a, array $b, array $expected): void {
|
||||
$this->mockUrlAssembler();
|
||||
|
||||
$link_a = new Link((new CacheableMetadata())->addCacheTags($a[0]), Url::fromUri($a[1]), $a[2]);
|
||||
$link_b = new Link((new CacheableMetadata())->addCacheTags($b[0]), Url::fromUri($b[1]), $b[2]);
|
||||
$link_expected = new Link((new CacheableMetadata())->addCacheTags($expected[0]), Url::fromUri($expected[1]), $expected[2]);
|
||||
|
||||
$this->assertSame($link_expected->getCacheTags(), Link::merge($link_a, $link_b)->getCacheTags());
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides test data for link merging.
|
||||
*/
|
||||
public static function linkMergeProvider(): \Generator {
|
||||
yield 'same everything' => [
|
||||
[['foo'], 'https://jsonapi.org/foo', 'self'],
|
||||
[['foo'], 'https://jsonapi.org/foo', 'self'],
|
||||
[['foo'], 'https://jsonapi.org/foo', 'self'],
|
||||
];
|
||||
yield 'different cache tags' => [
|
||||
[['foo'], 'https://jsonapi.org/foo', 'self'],
|
||||
[['bar'], 'https://jsonapi.org/foo', 'self'],
|
||||
[['foo', 'bar'], 'https://jsonapi.org/foo', 'self'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getLinkRelationType
|
||||
*/
|
||||
public function testGetLinkRelationType(): void {
|
||||
$this->mockUrlAssembler();
|
||||
$link = new Link((new CacheableMetadata())->addCacheTags(['foo']), Url::fromUri('https://jsonapi.org/foo'), 'self');
|
||||
$this->assertSame('self', $link->getLinkRelationType());
|
||||
}
|
||||
|
||||
/**
|
||||
* Mocks the unrouted URL assembler.
|
||||
*/
|
||||
protected function mockUrlAssembler(): void {
|
||||
$url_assembler = $this->getMockBuilder(UnroutedUrlAssemblerInterface::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$url_assembler->method('assemble')->willReturnCallback(function ($uri) {
|
||||
return (new GeneratedUrl())->setGeneratedUrl($uri);
|
||||
});
|
||||
|
||||
$container = new ContainerBuilder();
|
||||
$container->set('unrouted_url_assembler', $url_assembler);
|
||||
\Drupal::setContainer($container);
|
||||
}
|
||||
|
||||
}
|
||||
130
web/core/modules/jsonapi/tests/src/Unit/JsonApiSpecTest.php
Normal file
130
web/core/modules/jsonapi/tests/src/Unit/JsonApiSpecTest.php
Normal file
@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Unit;
|
||||
|
||||
use Drupal\jsonapi\JsonApiSpec;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
|
||||
// cspell:ignore kitt
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\jsonapi\JsonApiSpec
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class JsonApiSpecTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* Ensures that member names are properly validated.
|
||||
*
|
||||
* @dataProvider providerTestIsValidMemberName
|
||||
* @covers ::isValidMemberName
|
||||
*/
|
||||
public function testIsValidMemberName($member_name, $expected): void {
|
||||
$this->assertSame($expected, JsonApiSpec::isValidMemberName($member_name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for testIsValidMemberName.
|
||||
*/
|
||||
public static function providerTestIsValidMemberName() {
|
||||
// Copied from http://jsonapi.org/format/upcoming/#document-member-names.
|
||||
$data = [];
|
||||
$data['alphanumeric-lowercase'] = ['12kittens', TRUE];
|
||||
$data['alphanumeric-uppercase'] = ['12KITTENS', TRUE];
|
||||
$data['alphanumeric-mixed'] = ['12KiTtEnS', TRUE];
|
||||
$data['unicode-above-u+0080'] = ['12🐱🐱', TRUE];
|
||||
$data['hyphen-start'] = ['-kittens', FALSE];
|
||||
$data['hyphen-middle'] = ['kitt-ens', TRUE];
|
||||
$data['hyphen-end'] = ['kittens-', FALSE];
|
||||
$data['low-line-start'] = ['_kittens', FALSE];
|
||||
$data['low-line-middle'] = ['kitt_ens', TRUE];
|
||||
$data['low-line-end'] = ['kittens_', FALSE];
|
||||
$data['space-start'] = [' kittens', FALSE];
|
||||
$data['space-middle'] = ['kitt ens', TRUE];
|
||||
$data['space-end'] = ['kittens ', FALSE];
|
||||
|
||||
// Additional test cases.
|
||||
// @todo When D8 requires PHP >= 7, convert to \u{10FFFF}.
|
||||
$data['unicode-above-u+0080-highest-allowed'] = ["12", TRUE];
|
||||
$data['single-character'] = ['a', TRUE];
|
||||
|
||||
$unsafe_chars = [
|
||||
'+',
|
||||
',',
|
||||
'.',
|
||||
'[',
|
||||
']',
|
||||
'!',
|
||||
'"',
|
||||
'#',
|
||||
'$',
|
||||
'%',
|
||||
'&',
|
||||
'\'',
|
||||
'(',
|
||||
')',
|
||||
'*',
|
||||
'/',
|
||||
':',
|
||||
';',
|
||||
'<',
|
||||
'=',
|
||||
'>',
|
||||
'?',
|
||||
'@',
|
||||
'\\',
|
||||
'^',
|
||||
'`',
|
||||
'{',
|
||||
'|',
|
||||
'}',
|
||||
'~',
|
||||
];
|
||||
foreach ($unsafe_chars as $unsafe_char) {
|
||||
$data['unsafe-' . $unsafe_char] = ['kitt' . $unsafe_char . 'ens', FALSE];
|
||||
}
|
||||
|
||||
// The ASCII control characters are in the range 0x00 to 0x1F plus 0x7F.
|
||||
for ($ascii = 0; $ascii <= 0x1F; $ascii++) {
|
||||
$data['unsafe-ascii-control-' . $ascii] = ['kitt' . chr($ascii) . 'ens', FALSE];
|
||||
}
|
||||
$data['unsafe-ascii-control-' . 0x7F] = ['kitt' . chr(0x7F) . 'ens', FALSE];
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides test cases.
|
||||
*
|
||||
* @dataProvider providerTestIsValidCustomQueryParameter
|
||||
* @covers ::isValidCustomQueryParameter
|
||||
* @covers ::isValidMemberName
|
||||
*/
|
||||
public function testIsValidCustomQueryParameter($custom_query_parameter, $expected): void {
|
||||
$this->assertSame($expected, JsonApiSpec::isValidCustomQueryParameter($custom_query_parameter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for testIsValidCustomQueryParameter.
|
||||
*/
|
||||
public static function providerTestIsValidCustomQueryParameter() {
|
||||
$data = static::providerTestIsValidMemberName();
|
||||
|
||||
// All valid member names are also valid custom query parameters, except for
|
||||
// single-character ones.
|
||||
$data['single-character'][1] = FALSE;
|
||||
|
||||
// Custom query parameter test cases.
|
||||
$data['custom-query-parameter-lowercase'] = ['foobar', FALSE];
|
||||
$data['custom-query-parameter-dash'] = ['foo-bar', TRUE];
|
||||
$data['custom-query-parameter-underscore'] = ['foo_bar', TRUE];
|
||||
$data['custom-query-parameter-camel-case'] = ['fooBar', TRUE];
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Unit\Normalizer;
|
||||
|
||||
use Drupal\Core\Config\ConfigFactory;
|
||||
use Drupal\Core\Config\ImmutableConfig;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\jsonapi\Normalizer\HttpExceptionNormalizer;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\jsonapi\Normalizer\HttpExceptionNormalizer
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class HttpExceptionNormalizerTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* @covers ::normalize
|
||||
*/
|
||||
public function testNormalize(): void {
|
||||
$request_stack = $this->prophesize(RequestStack::class);
|
||||
$request_stack->getCurrentRequest()->willReturn(Request::create('http://localhost/'));
|
||||
$container = $this->prophesize(ContainerInterface::class);
|
||||
$container->get('request_stack')->willReturn($request_stack->reveal());
|
||||
$config = $this->prophesize(ImmutableConfig::class);
|
||||
$config->get('error_level')->willReturn(ERROR_REPORTING_DISPLAY_VERBOSE);
|
||||
$config_factory = $this->prophesize(ConfigFactory::class);
|
||||
$config_factory->get('system.logging')->willReturn($config->reveal());
|
||||
$container->get('config.factory')->willReturn($config_factory->reveal());
|
||||
\Drupal::setContainer($container->reveal());
|
||||
$exception = new AccessDeniedHttpException('lorem', NULL, 13);
|
||||
$current_user = $this->prophesize(AccountInterface::class);
|
||||
$current_user->hasPermission('access site reports')->willReturn(TRUE);
|
||||
$normalizer = new HttpExceptionNormalizer($current_user->reveal());
|
||||
$normalized = $normalizer->normalize($exception, 'api_json');
|
||||
$normalized = $normalized->getNormalization();
|
||||
$error = $normalized[0];
|
||||
$this->assertNotEmpty($error['meta']);
|
||||
$this->assertNotEmpty($error['source']);
|
||||
$this->assertSame('13', $error['code']);
|
||||
$this->assertSame('403', $error['status']);
|
||||
$this->assertEquals('Forbidden', $error['title']);
|
||||
$this->assertEquals('lorem', $error['detail']);
|
||||
$this->assertArrayHasKey('trace', $error['meta']);
|
||||
$this->assertNotEmpty($error['meta']['trace']);
|
||||
|
||||
$current_user = $this->prophesize(AccountInterface::class);
|
||||
$current_user->hasPermission('access site reports')->willReturn(FALSE);
|
||||
$normalizer = new HttpExceptionNormalizer($current_user->reveal());
|
||||
$normalized = $normalizer->normalize($exception, 'api_json');
|
||||
$normalized = $normalized->getNormalization();
|
||||
$error = $normalized[0];
|
||||
$this->assertArrayNotHasKey('meta', $error);
|
||||
$this->assertArrayNotHasKey('source', $error);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,261 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Unit\Normalizer;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Entity\FieldableEntityInterface;
|
||||
use Drupal\jsonapi\ResourceType\ResourceType;
|
||||
use Drupal\jsonapi\Normalizer\JsonApiDocumentTopLevelNormalizer;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Prophecy\Argument;
|
||||
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\jsonapi\Normalizer\JsonApiDocumentTopLevelNormalizer
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class JsonApiDocumentTopLevelNormalizerTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* The normalizer under test.
|
||||
*
|
||||
* @var \Drupal\jsonapi\Normalizer\JsonApiDocumentTopLevelNormalizer
|
||||
*/
|
||||
protected $normalizer;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$resource_type_repository = $this->prophesize(ResourceTypeRepository::class);
|
||||
|
||||
$resource_type_repository
|
||||
->getByTypeName(Argument::any())
|
||||
->willReturn(new ResourceType('node', 'article', NULL));
|
||||
|
||||
$entity_storage = $this->prophesize(EntityStorageInterface::class);
|
||||
$self = $this;
|
||||
$uuid_to_id = [
|
||||
'76dd5c18-ea1b-4150-9e75-b21958a2b836' => 1,
|
||||
'fcce1b61-258e-4054-ae36-244d25a9e04c' => 2,
|
||||
];
|
||||
$entity_storage->loadByProperties(Argument::type('array'))
|
||||
->will(function ($args) use ($self, $uuid_to_id) {
|
||||
$result = [];
|
||||
foreach ($args[0]['uuid'] as $uuid) {
|
||||
$entity = $self->prophesize(EntityInterface::class);
|
||||
$entity->uuid()->willReturn($uuid);
|
||||
$entity->id()->willReturn($uuid_to_id[$uuid]);
|
||||
$result[$uuid] = $entity->reveal();
|
||||
}
|
||||
return $result;
|
||||
});
|
||||
$entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class);
|
||||
$entity_type_manager->getStorage('node')->willReturn($entity_storage->reveal());
|
||||
$entity_type = $this->prophesize(EntityTypeInterface::class);
|
||||
$entity_type->getKey('uuid')->willReturn('uuid');
|
||||
$entity_type_manager->getDefinition('node')->willReturn($entity_type->reveal());
|
||||
|
||||
$this->normalizer = new JsonApiDocumentTopLevelNormalizer(
|
||||
$entity_type_manager->reveal(),
|
||||
$resource_type_repository->reveal()
|
||||
);
|
||||
|
||||
$serializer = $this->prophesize(DenormalizerInterface::class);
|
||||
$serializer->willImplement(SerializerInterface::class);
|
||||
$serializer->denormalize(
|
||||
Argument::type('array'),
|
||||
Argument::type('string'),
|
||||
Argument::type('string'),
|
||||
Argument::type('array')
|
||||
)->willReturnArgument(0);
|
||||
|
||||
$this->normalizer->setSerializer($serializer->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::denormalize
|
||||
* @dataProvider denormalizeProvider
|
||||
*/
|
||||
public function testDenormalize($input, $expected): void {
|
||||
$resource_type = new ResourceType('node', 'article', FieldableEntityInterface::class);
|
||||
$resource_type->setRelatableResourceTypes([]);
|
||||
$context = ['resource_type' => $resource_type];
|
||||
$denormalized = $this->normalizer->denormalize($input, NULL, 'api_json', $context);
|
||||
$this->assertSame($expected, $denormalized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for the denormalize test.
|
||||
*
|
||||
* @return array
|
||||
* The data for the test method.
|
||||
*/
|
||||
public static function denormalizeProvider() {
|
||||
return [
|
||||
[
|
||||
[
|
||||
'data' => [
|
||||
'type' => 'lorem',
|
||||
'id' => 'e1a613f6-f2b9-4e17-9d33-727eb6509d8b',
|
||||
'attributes' => ['title' => 'dummy_title'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'title' => 'dummy_title',
|
||||
'uuid' => 'e1a613f6-f2b9-4e17-9d33-727eb6509d8b',
|
||||
],
|
||||
],
|
||||
[
|
||||
[
|
||||
'data' => [
|
||||
'type' => 'lorem',
|
||||
'id' => '0676d1bf-55b3-4bbc-9fbc-3df10f4599d5',
|
||||
'relationships' => [
|
||||
'field_dummy' => [
|
||||
'data' => [
|
||||
'type' => 'node',
|
||||
'id' => '76dd5c18-ea1b-4150-9e75-b21958a2b836',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'uuid' => '0676d1bf-55b3-4bbc-9fbc-3df10f4599d5',
|
||||
'field_dummy' => [
|
||||
[
|
||||
'target_id' => 1,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
[
|
||||
'data' => [
|
||||
'type' => 'lorem',
|
||||
'id' => '535ba297-8d79-4fc1-b0d6-dc2f047765a1',
|
||||
'relationships' => [
|
||||
'field_dummy' => [
|
||||
'data' => [
|
||||
[
|
||||
'type' => 'node',
|
||||
'id' => '76dd5c18-ea1b-4150-9e75-b21958a2b836',
|
||||
],
|
||||
[
|
||||
'type' => 'node',
|
||||
'id' => 'fcce1b61-258e-4054-ae36-244d25a9e04c',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'uuid' => '535ba297-8d79-4fc1-b0d6-dc2f047765a1',
|
||||
'field_dummy' => [
|
||||
['target_id' => 1],
|
||||
['target_id' => 2],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
[
|
||||
'data' => [
|
||||
'type' => 'lorem',
|
||||
'id' => '535ba297-8d79-4fc1-b0d6-dc2f047765a1',
|
||||
'relationships' => [
|
||||
'field_dummy' => [
|
||||
'data' => [
|
||||
[
|
||||
'type' => 'node',
|
||||
'id' => '76dd5c18-ea1b-4150-9e75-b21958a2b836',
|
||||
'meta' => ['foo' => 'bar'],
|
||||
],
|
||||
[
|
||||
'type' => 'node',
|
||||
'id' => 'fcce1b61-258e-4054-ae36-244d25a9e04c',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'uuid' => '535ba297-8d79-4fc1-b0d6-dc2f047765a1',
|
||||
'field_dummy' => [
|
||||
[
|
||||
'target_id' => 1,
|
||||
'foo' => 'bar',
|
||||
],
|
||||
['target_id' => 2],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures only valid UUIDs can be specified.
|
||||
*
|
||||
* @param string $id
|
||||
* The input UUID. May be invalid.
|
||||
* @param bool $expect_exception
|
||||
* Whether to expect an exception.
|
||||
*
|
||||
* @covers ::denormalize
|
||||
* @dataProvider denormalizeUuidProvider
|
||||
*/
|
||||
public function testDenormalizeUuid($id, $expect_exception): void {
|
||||
$data['data'] = (isset($id)) ?
|
||||
['type' => 'node--article', 'id' => $id] :
|
||||
['type' => 'node--article'];
|
||||
|
||||
if ($expect_exception) {
|
||||
$this->expectException(UnprocessableEntityHttpException::class);
|
||||
$this->expectExceptionMessage('IDs should be properly generated and formatted UUIDs as described in RFC 4122.');
|
||||
}
|
||||
|
||||
$denormalized = $this->normalizer->denormalize($data, NULL, 'api_json', [
|
||||
'resource_type' => new ResourceType(
|
||||
'node',
|
||||
'article',
|
||||
FieldableEntityInterface::class
|
||||
),
|
||||
]);
|
||||
|
||||
if (isset($id)) {
|
||||
$this->assertSame($id, $denormalized['uuid']);
|
||||
}
|
||||
else {
|
||||
$this->assertArrayNotHasKey('uuid', $denormalized);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides test cases for testDenormalizeUuid.
|
||||
*/
|
||||
public static function denormalizeUuidProvider() {
|
||||
return [
|
||||
'valid' => ['76dd5c18-ea1b-4150-9e75-b21958a2b836', FALSE],
|
||||
'missing' => [NULL, FALSE],
|
||||
'invalid_empty' => ['', TRUE],
|
||||
'invalid_alpha' => ['invalid', TRUE],
|
||||
'invalid_numeric' => [1234, TRUE],
|
||||
'invalid_alphanumeric' => ['abc123', TRUE],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,207 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Unit\Normalizer;
|
||||
|
||||
use Drupal\Core\Entity\EntityFieldManagerInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityRepositoryInterface;
|
||||
use Drupal\Core\Entity\FieldableEntityInterface;
|
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
||||
use Drupal\Core\Field\FieldTypePluginManagerInterface;
|
||||
use Drupal\Core\Field\TypedData\FieldItemDataDefinition;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\jsonapi\JsonApiResource\ResourceIdentifier;
|
||||
use Drupal\jsonapi\Normalizer\ResourceIdentifierNormalizer;
|
||||
use Drupal\jsonapi\ResourceType\ResourceType;
|
||||
use Drupal\jsonapi\ResourceType\ResourceTypeRelationship;
|
||||
use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Prophecy\Argument;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\jsonapi\Normalizer\ResourceIdentifierNormalizer
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ResourceIdentifierNormalizerTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* The normalizer under test.
|
||||
*
|
||||
* @var \Drupal\jsonapi\Normalizer\EntityReferenceFieldNormalizer
|
||||
*/
|
||||
protected $normalizer;
|
||||
|
||||
/**
|
||||
* The base resource type for testing.
|
||||
*
|
||||
* @var \Drupal\jsonapi\ResourceType\ResourceType
|
||||
*/
|
||||
protected $resourceType;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$target_resource_type = new ResourceType('lorem', 'dummy_bundle', NULL);
|
||||
$relationship_fields = [
|
||||
'field_dummy' => new ResourceTypeRelationship('field_dummy'),
|
||||
'field_dummy_single' => new ResourceTypeRelationship('field_dummy_single'),
|
||||
];
|
||||
$this->resourceType = new ResourceType('fake_entity_type', 'dummy_bundle', NULL, FALSE, TRUE, TRUE, FALSE, $relationship_fields);
|
||||
$this->resourceType->setRelatableResourceTypes([
|
||||
'field_dummy' => [$target_resource_type],
|
||||
'field_dummy_single' => [$target_resource_type],
|
||||
]);
|
||||
|
||||
$field_manager = $this->prophesize(EntityFieldManagerInterface::class);
|
||||
$field_definition = $this->prophesize(FieldConfig::class);
|
||||
$item_definition = $this->prophesize(FieldItemDataDefinition::class);
|
||||
$item_definition->getMainPropertyName()->willReturn('bunny');
|
||||
$item_definition->getSetting('target_type')->willReturn('fake_entity_type');
|
||||
$item_definition->getSetting('handler_settings')->willReturn([
|
||||
'target_bundles' => ['dummy_bundle'],
|
||||
]);
|
||||
$field_definition->getItemDefinition()
|
||||
->willReturn($item_definition->reveal());
|
||||
$storage_definition = $this->prophesize(FieldStorageDefinitionInterface::class);
|
||||
$storage_definition->isMultiple()->willReturn(TRUE);
|
||||
$field_definition->getFieldStorageDefinition()->willReturn($storage_definition->reveal());
|
||||
|
||||
$field_definition2 = $this->prophesize(FieldConfig::class);
|
||||
$field_definition2->getItemDefinition()
|
||||
->willReturn($item_definition->reveal());
|
||||
$storage_definition2 = $this->prophesize(FieldStorageDefinitionInterface::class);
|
||||
$storage_definition2->isMultiple()->willReturn(FALSE);
|
||||
$field_definition2->getFieldStorageDefinition()->willReturn($storage_definition2->reveal());
|
||||
|
||||
$field_manager->getFieldDefinitions('fake_entity_type', 'dummy_bundle')
|
||||
->willReturn([
|
||||
'field_dummy' => $field_definition->reveal(),
|
||||
'field_dummy_single' => $field_definition2->reveal(),
|
||||
]);
|
||||
$plugin_manager = $this->prophesize(FieldTypePluginManagerInterface::class);
|
||||
$plugin_manager->createFieldItemList(
|
||||
Argument::type(FieldableEntityInterface::class),
|
||||
Argument::type('string'),
|
||||
Argument::type('array')
|
||||
)->willReturnArgument(2);
|
||||
$resource_type_repository = $this->prophesize(ResourceTypeRepository::class);
|
||||
$resource_type_repository->get('fake_entity_type', 'dummy_bundle')->willReturn($this->resourceType);
|
||||
|
||||
$entity = $this->prophesize(EntityInterface::class);
|
||||
$entity->uuid()->willReturn('4e6cb61d-4f04-437f-99fe-42c002393658');
|
||||
$entity->id()->willReturn(42);
|
||||
$entity_repository = $this->prophesize(EntityRepositoryInterface::class);
|
||||
$entity_repository->loadEntityByUuid('lorem', '4e6cb61d-4f04-437f-99fe-42c002393658')
|
||||
->willReturn($entity->reveal());
|
||||
|
||||
$this->normalizer = new ResourceIdentifierNormalizer(
|
||||
$field_manager->reveal()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::denormalize
|
||||
* @dataProvider denormalizeProvider
|
||||
*/
|
||||
public function testDenormalize($input, $field_name, $expected): void {
|
||||
$entity = $this->prophesize(FieldableEntityInterface::class);
|
||||
$context = [
|
||||
'resource_type' => $this->resourceType,
|
||||
'related' => $field_name,
|
||||
'target_entity' => $entity->reveal(),
|
||||
];
|
||||
$denormalized = $this->normalizer->denormalize($input, NULL, 'api_json', $context);
|
||||
$this->assertEquals($expected, $denormalized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for the denormalize test.
|
||||
*
|
||||
* @return array
|
||||
* The data for the test method.
|
||||
*/
|
||||
public static function denormalizeProvider() {
|
||||
return [
|
||||
[
|
||||
['data' => [['type' => 'lorem--dummy_bundle', 'id' => '4e6cb61d-4f04-437f-99fe-42c002393658']]],
|
||||
'field_dummy',
|
||||
[new ResourceIdentifier('lorem--dummy_bundle', '4e6cb61d-4f04-437f-99fe-42c002393658')],
|
||||
],
|
||||
[
|
||||
['data' => []],
|
||||
'field_dummy',
|
||||
[],
|
||||
],
|
||||
[
|
||||
['data' => NULL],
|
||||
'field_dummy_single',
|
||||
[],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::denormalize
|
||||
* @dataProvider denormalizeInvalidResourceProvider
|
||||
*/
|
||||
public function testDenormalizeInvalidResource($data, $field_name): void {
|
||||
$context = [
|
||||
'resource_type' => $this->resourceType,
|
||||
'related' => $field_name,
|
||||
'target_entity' => $this->prophesize(FieldableEntityInterface::class)->reveal(),
|
||||
];
|
||||
$this->expectException(BadRequestHttpException::class);
|
||||
$this->normalizer->denormalize($data, NULL, 'api_json', $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for the denormalize test.
|
||||
*
|
||||
* @return array
|
||||
* The input data for the test method.
|
||||
*/
|
||||
public static function denormalizeInvalidResourceProvider() {
|
||||
return [
|
||||
[
|
||||
[
|
||||
'data' => [
|
||||
[
|
||||
'type' => 'invalid',
|
||||
'id' => '4e6cb61d-4f04-437f-99fe-42c002393658',
|
||||
],
|
||||
],
|
||||
],
|
||||
'field_dummy',
|
||||
],
|
||||
[
|
||||
[
|
||||
'data' => [
|
||||
'type' => 'lorem',
|
||||
'id' => '4e6cb61d-4f04-437f-99fe-42c002393658',
|
||||
],
|
||||
],
|
||||
'field_dummy',
|
||||
],
|
||||
[
|
||||
[
|
||||
'data' => [
|
||||
[
|
||||
'type' => 'lorem',
|
||||
'id' => '4e6cb61d-4f04-437f-99fe-42c002393658',
|
||||
],
|
||||
],
|
||||
],
|
||||
'field_dummy_single',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Unit\Query;
|
||||
|
||||
use Drupal\jsonapi\Query\EntityConditionGroup;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\jsonapi\Query\EntityConditionGroup
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class EntityConditionGroupTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* @covers ::__construct
|
||||
* @dataProvider constructProvider
|
||||
*/
|
||||
public function testConstruct($case): void {
|
||||
$group = new EntityConditionGroup($case['conjunction'], $case['members']);
|
||||
|
||||
$this->assertEquals($case['conjunction'], $group->conjunction());
|
||||
|
||||
foreach ($group->members() as $key => $condition) {
|
||||
$this->assertEquals($case['members'][$key]['path'], $condition->field());
|
||||
$this->assertEquals($case['members'][$key]['value'], $condition->value());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__construct
|
||||
*/
|
||||
public function testConstructException(): void {
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
new EntityConditionGroup('NOT_ALLOWED', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for testConstruct.
|
||||
*/
|
||||
public static function constructProvider() {
|
||||
return [
|
||||
[['conjunction' => 'AND', 'members' => []]],
|
||||
[['conjunction' => 'OR', 'members' => []]],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Unit\Query;
|
||||
|
||||
use Drupal\Core\Cache\Context\CacheContextsManager;
|
||||
use Drupal\Core\DependencyInjection\Container;
|
||||
use Drupal\jsonapi\Query\EntityCondition;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Prophecy\Argument;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\jsonapi\Query\EntityCondition
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class EntityConditionTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$container = new Container();
|
||||
$cache_context_manager = $this->prophesize(CacheContextsManager::class);
|
||||
$cache_context_manager->assertValidTokens(Argument::any())
|
||||
->willReturn(TRUE);
|
||||
$container->set('cache_contexts_manager', $cache_context_manager->reveal());
|
||||
\Drupal::setContainer($container);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::createFromQueryParameter
|
||||
* @dataProvider queryParameterProvider
|
||||
*/
|
||||
public function testCreateFromQueryParameter($case): void {
|
||||
$condition = EntityCondition::createFromQueryParameter($case);
|
||||
$this->assertEquals($case['path'], $condition->field());
|
||||
$this->assertEquals($case['value'], $condition->value());
|
||||
if (isset($case['operator'])) {
|
||||
$this->assertEquals($case['operator'], $condition->operator());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for testDenormalize.
|
||||
*/
|
||||
public static function queryParameterProvider() {
|
||||
return [
|
||||
[['path' => 'some_field', 'value' => NULL, 'operator' => '=']],
|
||||
[['path' => 'some_field', 'operator' => '=', 'value' => 'some_string']],
|
||||
[['path' => 'some_field', 'operator' => '<>', 'value' => 'some_string']],
|
||||
[
|
||||
[
|
||||
'path' => 'some_field',
|
||||
'operator' => 'NOT BETWEEN',
|
||||
'value' => 'some_string',
|
||||
],
|
||||
],
|
||||
[
|
||||
[
|
||||
'path' => 'some_field',
|
||||
'operator' => 'BETWEEN',
|
||||
'value' => ['some_string'],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::validate
|
||||
* @dataProvider validationProvider
|
||||
*/
|
||||
public function testValidation($input, $exception): void {
|
||||
if ($exception) {
|
||||
$this->expectException(get_class($exception));
|
||||
$this->expectExceptionMessage($exception->getMessage());
|
||||
}
|
||||
EntityCondition::createFromQueryParameter($input);
|
||||
$this->assertNull($exception, 'No exception was expected.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for testValidation.
|
||||
*/
|
||||
public static function validationProvider() {
|
||||
return [
|
||||
[['path' => 'some_field', 'value' => 'some_value'], NULL],
|
||||
[
|
||||
['path' => 'some_field', 'value' => 'some_value', 'operator' => '='],
|
||||
NULL,
|
||||
],
|
||||
[['path' => 'some_field', 'operator' => 'IS NULL'], NULL],
|
||||
[['path' => 'some_field', 'operator' => 'IS NOT NULL'], NULL],
|
||||
[
|
||||
['path' => 'some_field', 'operator' => 'IS', 'value' => 'some_value'],
|
||||
new BadRequestHttpException("The 'IS' operator is not allowed in a filter parameter."),
|
||||
],
|
||||
[
|
||||
[
|
||||
'path' => 'some_field',
|
||||
'operator' => 'NOT_ALLOWED',
|
||||
'value' => 'some_value',
|
||||
],
|
||||
new BadRequestHttpException("The 'NOT_ALLOWED' operator is not allowed in a filter parameter."),
|
||||
],
|
||||
[
|
||||
[
|
||||
'path' => 'some_field',
|
||||
'operator' => 'IS NULL',
|
||||
'value' => 'should_not_be_here',
|
||||
],
|
||||
new BadRequestHttpException("Filters using the 'IS NULL' operator should not provide a value."),
|
||||
],
|
||||
[
|
||||
[
|
||||
'path' => 'some_field',
|
||||
'operator' => 'IS NOT NULL',
|
||||
'value' => 'should_not_be_here',
|
||||
],
|
||||
new BadRequestHttpException("Filters using the 'IS NOT NULL' operator should not provide a value."),
|
||||
],
|
||||
[
|
||||
['path' => 'path_only'],
|
||||
new BadRequestHttpException("Filter parameter is missing a '" . EntityCondition::VALUE_KEY . "' key."),
|
||||
],
|
||||
[
|
||||
['value' => 'value_only'],
|
||||
new BadRequestHttpException("Filter parameter is missing a '" . EntityCondition::PATH_KEY . "' key."),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Unit\Query;
|
||||
|
||||
use Drupal\Core\Cache\Context\CacheContextsManager;
|
||||
use Drupal\Core\DependencyInjection\Container;
|
||||
use Drupal\jsonapi\Query\OffsetPage;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Prophecy\Argument;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\jsonapi\Query\OffsetPage
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class OffsetPageTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$container = new Container();
|
||||
$cache_context_manager = $this->prophesize(CacheContextsManager::class);
|
||||
$cache_context_manager->assertValidTokens(Argument::any())
|
||||
->willReturn(TRUE);
|
||||
$container->set('cache_contexts_manager', $cache_context_manager->reveal());
|
||||
\Drupal::setContainer($container);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::createFromQueryParameter
|
||||
* @dataProvider parameterProvider
|
||||
*/
|
||||
public function testCreateFromQueryParameter($original, $expected): void {
|
||||
$actual = OffsetPage::createFromQueryParameter($original);
|
||||
$this->assertEquals($expected['offset'], $actual->getOffset());
|
||||
$this->assertEquals($expected['limit'], $actual->getSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for testCreateFromQueryParameter.
|
||||
*/
|
||||
public static function parameterProvider() {
|
||||
return [
|
||||
[['offset' => 12, 'limit' => 20], ['offset' => 12, 'limit' => 20]],
|
||||
[['offset' => 12, 'limit' => 60], ['offset' => 12, 'limit' => 50]],
|
||||
[['offset' => 12], ['offset' => 12, 'limit' => 50]],
|
||||
[['offset' => 0], ['offset' => 0, 'limit' => 50]],
|
||||
[[], ['offset' => 0, 'limit' => 50]],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::createFromQueryParameter
|
||||
*/
|
||||
public function testCreateFromQueryParameterFail(): void {
|
||||
$this->expectException(BadRequestHttpException::class);
|
||||
OffsetPage::createFromQueryParameter('lorem');
|
||||
}
|
||||
|
||||
}
|
||||
103
web/core/modules/jsonapi/tests/src/Unit/Query/SortTest.php
Normal file
103
web/core/modules/jsonapi/tests/src/Unit/Query/SortTest.php
Normal file
@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Unit\Query;
|
||||
|
||||
use Drupal\Core\Cache\Context\CacheContextsManager;
|
||||
use Drupal\Core\DependencyInjection\Container;
|
||||
use Drupal\jsonapi\Query\Sort;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Prophecy\Argument;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\jsonapi\Query\Sort
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class SortTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$container = new Container();
|
||||
$cache_context_manager = $this->prophesize(CacheContextsManager::class);
|
||||
$cache_context_manager->assertValidTokens(Argument::any())
|
||||
->willReturn(TRUE);
|
||||
$container->set('cache_contexts_manager', $cache_context_manager->reveal());
|
||||
\Drupal::setContainer($container);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::createFromQueryParameter
|
||||
* @dataProvider parameterProvider
|
||||
*/
|
||||
public function testCreateFromQueryParameter($input, $expected): void {
|
||||
$sort = Sort::createFromQueryParameter($input);
|
||||
foreach ($sort->fields() as $index => $sort_field) {
|
||||
$this->assertEquals($expected[$index]['path'], $sort_field['path']);
|
||||
$this->assertEquals($expected[$index]['direction'], $sort_field['direction']);
|
||||
$this->assertEquals($expected[$index]['langcode'], $sort_field['langcode']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a suite of shortcut sort parameters and their expected expansions.
|
||||
*/
|
||||
public static function parameterProvider() {
|
||||
return [
|
||||
['lorem', [['path' => 'lorem', 'direction' => 'ASC', 'langcode' => NULL]]],
|
||||
[
|
||||
'-lorem',
|
||||
[['path' => 'lorem', 'direction' => 'DESC', 'langcode' => NULL]],
|
||||
],
|
||||
['-lorem,ipsum', [
|
||||
['path' => 'lorem', 'direction' => 'DESC', 'langcode' => NULL],
|
||||
['path' => 'ipsum', 'direction' => 'ASC', 'langcode' => NULL],
|
||||
],
|
||||
],
|
||||
['-lorem,-ipsum', [
|
||||
['path' => 'lorem', 'direction' => 'DESC', 'langcode' => NULL],
|
||||
['path' => 'ipsum', 'direction' => 'DESC', 'langcode' => NULL],
|
||||
],
|
||||
],
|
||||
[[
|
||||
['path' => 'lorem', 'langcode' => NULL],
|
||||
['path' => 'ipsum', 'langcode' => 'ca'],
|
||||
['path' => 'dolor', 'direction' => 'ASC', 'langcode' => 'ca'],
|
||||
['path' => 'sit', 'direction' => 'DESC', 'langcode' => 'ca'],
|
||||
], [
|
||||
['path' => 'lorem', 'direction' => 'ASC', 'langcode' => NULL],
|
||||
['path' => 'ipsum', 'direction' => 'ASC', 'langcode' => 'ca'],
|
||||
['path' => 'dolor', 'direction' => 'ASC', 'langcode' => 'ca'],
|
||||
['path' => 'sit', 'direction' => 'DESC', 'langcode' => 'ca'],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::createFromQueryParameter
|
||||
* @dataProvider badParameterProvider
|
||||
*/
|
||||
public function testCreateFromQueryParameterFail($input): void {
|
||||
$this->expectException(BadRequestHttpException::class);
|
||||
Sort::createFromQueryParameter($input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for testCreateFromQueryParameterFail.
|
||||
*/
|
||||
public static function badParameterProvider() {
|
||||
return [
|
||||
[[['lorem']]],
|
||||
[''],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
245
web/core/modules/jsonapi/tests/src/Unit/Routing/RoutesTest.php
Normal file
245
web/core/modules/jsonapi/tests/src/Unit/Routing/RoutesTest.php
Normal file
@ -0,0 +1,245 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\jsonapi\Unit\Routing;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\jsonapi\ResourceType\ResourceType;
|
||||
use Drupal\jsonapi\ResourceType\ResourceTypeRelationship;
|
||||
use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
|
||||
use Drupal\jsonapi\Routing\Routes;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Drupal\Core\Routing\RouteObjectInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\jsonapi\Routing\Routes
|
||||
* @group jsonapi
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class RoutesTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* List of routes objects for the different scenarios.
|
||||
*
|
||||
* @var \Drupal\jsonapi\Routing\Routes[]
|
||||
*/
|
||||
protected $routes;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$relationship_fields = [
|
||||
'external' => new ResourceTypeRelationship('external'),
|
||||
'internal' => new ResourceTypeRelationship('internal'),
|
||||
'both' => new ResourceTypeRelationship('both'),
|
||||
];
|
||||
$type_1 = new ResourceType('entity_type_1', 'bundle_1_1', EntityInterface::class, FALSE, TRUE, TRUE, FALSE, $relationship_fields);
|
||||
$type_2 = new ResourceType('entity_type_2', 'bundle_2_1', EntityInterface::class, TRUE, TRUE, TRUE, FALSE, $relationship_fields);
|
||||
$relatable_resource_types = [
|
||||
'external' => [$type_1],
|
||||
'internal' => [$type_2],
|
||||
'both' => [$type_1, $type_2],
|
||||
];
|
||||
$type_1->setRelatableResourceTypes($relatable_resource_types);
|
||||
$type_2->setRelatableResourceTypes($relatable_resource_types);
|
||||
// This type ensures that we can create routes for bundle IDs which might be
|
||||
// cast from strings to integers. It should not affect related resource
|
||||
// routing.
|
||||
$type_3 = new ResourceType('entity_type_3', '123', EntityInterface::class, TRUE);
|
||||
$type_3->setRelatableResourceTypes([]);
|
||||
$resource_type_repository = $this->prophesize(ResourceTypeRepository::class);
|
||||
$resource_type_repository->all()->willReturn([$type_1, $type_2, $type_3]);
|
||||
$container = $this->prophesize(ContainerInterface::class);
|
||||
$container->get('jsonapi.resource_type.repository')->willReturn($resource_type_repository->reveal());
|
||||
$container->getParameter('jsonapi.base_path')->willReturn('/jsonapi');
|
||||
$container->getParameter('authentication_providers')->willReturn([
|
||||
'lorem' => [],
|
||||
'ipsum' => [],
|
||||
]);
|
||||
|
||||
$this->routes['ok'] = Routes::create($container->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::routes
|
||||
*/
|
||||
public function testRoutesCollection(): void {
|
||||
// Get the route collection and start making assertions.
|
||||
$routes = $this->routes['ok']->routes();
|
||||
|
||||
// - 2 collection routes; GET & POST for the non-internal resource type.
|
||||
// - 3 individual routes; GET, PATCH & DELETE for the non-internal resource
|
||||
// type.
|
||||
// - 2 related routes; GET for the non-internal resource type relationships
|
||||
// fields: external & both.
|
||||
// - 12 relationship routes; 3 fields * 4 HTTP methods.
|
||||
// `relationship` routes are generated even for internal target resource
|
||||
// types (`related` routes are not).
|
||||
// - 1 for the JSON:API entry point.
|
||||
$this->assertEquals(20, $routes->count());
|
||||
|
||||
$iterator = $routes->getIterator();
|
||||
// Check the collection route.
|
||||
/** @var \Symfony\Component\Routing\Route $route */
|
||||
$route = $iterator->offsetGet('jsonapi.entity_type_1--bundle_1_1.collection');
|
||||
$this->assertSame('/jsonapi/entity_type_1/bundle_1_1', $route->getPath());
|
||||
$this->assertSame(['lorem', 'ipsum'], $route->getOption('_auth'));
|
||||
$this->assertSame('entity_type_1--bundle_1_1', $route->getDefault(Routes::RESOURCE_TYPE_KEY));
|
||||
$this->assertEquals(['GET'], $route->getMethods());
|
||||
$this->assertSame(Routes::CONTROLLER_SERVICE_NAME . ':getCollection', $route->getDefault(RouteObjectInterface::CONTROLLER_NAME));
|
||||
// Check the collection POST route.
|
||||
$route = $iterator->offsetGet('jsonapi.entity_type_1--bundle_1_1.collection.post');
|
||||
$this->assertSame('/jsonapi/entity_type_1/bundle_1_1', $route->getPath());
|
||||
$this->assertSame(['lorem', 'ipsum'], $route->getOption('_auth'));
|
||||
$this->assertSame('entity_type_1--bundle_1_1', $route->getDefault(Routes::RESOURCE_TYPE_KEY));
|
||||
$this->assertEquals(['POST'], $route->getMethods());
|
||||
$this->assertSame(Routes::CONTROLLER_SERVICE_NAME . ':createIndividual', $route->getDefault(RouteObjectInterface::CONTROLLER_NAME));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::routes
|
||||
*/
|
||||
public function testRoutesIndividual(): void {
|
||||
// Get the route collection and start making assertions.
|
||||
$iterator = $this->routes['ok']->routes()->getIterator();
|
||||
|
||||
// Check the individual route.
|
||||
/** @var \Symfony\Component\Routing\Route $route */
|
||||
$route = $iterator->offsetGet('jsonapi.entity_type_1--bundle_1_1.individual');
|
||||
$this->assertSame('/jsonapi/entity_type_1/bundle_1_1/{entity}', $route->getPath());
|
||||
$this->assertSame('entity_type_1--bundle_1_1', $route->getDefault(Routes::RESOURCE_TYPE_KEY));
|
||||
$this->assertEquals(['GET'], $route->getMethods());
|
||||
$this->assertSame(Routes::CONTROLLER_SERVICE_NAME . ':getIndividual', $route->getDefault(RouteObjectInterface::CONTROLLER_NAME));
|
||||
$this->assertSame(['lorem', 'ipsum'], $route->getOption('_auth'));
|
||||
$this->assertEquals([
|
||||
'entity' => ['type' => 'entity:entity_type_1'],
|
||||
'resource_type' => ['type' => 'jsonapi_resource_type'],
|
||||
], $route->getOption('parameters'));
|
||||
|
||||
$route = $iterator->offsetGet('jsonapi.entity_type_1--bundle_1_1.individual.patch');
|
||||
$this->assertSame('/jsonapi/entity_type_1/bundle_1_1/{entity}', $route->getPath());
|
||||
$this->assertSame('entity_type_1--bundle_1_1', $route->getDefault(Routes::RESOURCE_TYPE_KEY));
|
||||
$this->assertEquals(['PATCH'], $route->getMethods());
|
||||
$this->assertSame(Routes::CONTROLLER_SERVICE_NAME . ':patchIndividual', $route->getDefault(RouteObjectInterface::CONTROLLER_NAME));
|
||||
$this->assertSame(['lorem', 'ipsum'], $route->getOption('_auth'));
|
||||
$this->assertEquals([
|
||||
'entity' => ['type' => 'entity:entity_type_1'],
|
||||
'resource_type' => ['type' => 'jsonapi_resource_type'],
|
||||
], $route->getOption('parameters'));
|
||||
|
||||
$route = $iterator->offsetGet('jsonapi.entity_type_1--bundle_1_1.individual.delete');
|
||||
$this->assertSame('/jsonapi/entity_type_1/bundle_1_1/{entity}', $route->getPath());
|
||||
$this->assertSame('entity_type_1--bundle_1_1', $route->getDefault(Routes::RESOURCE_TYPE_KEY));
|
||||
$this->assertEquals(['DELETE'], $route->getMethods());
|
||||
$this->assertSame(Routes::CONTROLLER_SERVICE_NAME . ':deleteIndividual', $route->getDefault(RouteObjectInterface::CONTROLLER_NAME));
|
||||
$this->assertSame(['lorem', 'ipsum'], $route->getOption('_auth'));
|
||||
$this->assertEquals([
|
||||
'entity' => ['type' => 'entity:entity_type_1'],
|
||||
'resource_type' => ['type' => 'jsonapi_resource_type'],
|
||||
], $route->getOption('parameters'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::routes
|
||||
*/
|
||||
public function testRoutesRelated(): void {
|
||||
// Get the route collection and start making assertions.
|
||||
$iterator = $this->routes['ok']->routes()->getIterator();
|
||||
|
||||
// Check the related route.
|
||||
/** @var \Symfony\Component\Routing\Route $route */
|
||||
$route = $iterator->offsetGet('jsonapi.entity_type_1--bundle_1_1.external.related');
|
||||
$this->assertSame('/jsonapi/entity_type_1/bundle_1_1/{entity}/external', $route->getPath());
|
||||
$this->assertSame('entity_type_1--bundle_1_1', $route->getDefault(Routes::RESOURCE_TYPE_KEY));
|
||||
$this->assertEquals(['GET'], $route->getMethods());
|
||||
$this->assertSame(Routes::CONTROLLER_SERVICE_NAME . ':getRelated', $route->getDefault(RouteObjectInterface::CONTROLLER_NAME));
|
||||
$this->assertSame(['lorem', 'ipsum'], $route->getOption('_auth'));
|
||||
$this->assertEquals([
|
||||
'entity' => ['type' => 'entity:entity_type_1'],
|
||||
'resource_type' => ['type' => 'jsonapi_resource_type'],
|
||||
], $route->getOption('parameters'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::routes
|
||||
*/
|
||||
public function testRoutesRelationships(): void {
|
||||
// Get the route collection and start making assertions.
|
||||
$iterator = $this->routes['ok']->routes()->getIterator();
|
||||
|
||||
// Check the relationships route.
|
||||
/** @var \Symfony\Component\Routing\Route $route */
|
||||
$route = $iterator->offsetGet('jsonapi.entity_type_1--bundle_1_1.both.relationship.get');
|
||||
$this->assertSame('/jsonapi/entity_type_1/bundle_1_1/{entity}/relationships/both', $route->getPath());
|
||||
$this->assertSame('entity_type_1--bundle_1_1', $route->getDefault(Routes::RESOURCE_TYPE_KEY));
|
||||
$this->assertEquals(['GET'], $route->getMethods());
|
||||
$this->assertSame(Routes::CONTROLLER_SERVICE_NAME . ':getRelationship', $route->getDefault(RouteObjectInterface::CONTROLLER_NAME));
|
||||
$this->assertSame(['lorem', 'ipsum'], $route->getOption('_auth'));
|
||||
$this->assertEquals([
|
||||
'entity' => ['type' => 'entity:entity_type_1'],
|
||||
'resource_type' => ['type' => 'jsonapi_resource_type'],
|
||||
], $route->getOption('parameters'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the expected routes are created or not created.
|
||||
*
|
||||
* @dataProvider expectedRoutes
|
||||
*/
|
||||
public function testRoutes($route): void {
|
||||
$this->assertArrayHasKey($route, $this->routes['ok']->routes()->all());
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists routes which should have been created.
|
||||
*/
|
||||
public static function expectedRoutes() {
|
||||
return [
|
||||
['jsonapi.entity_type_1--bundle_1_1.individual'],
|
||||
['jsonapi.entity_type_1--bundle_1_1.collection'],
|
||||
['jsonapi.entity_type_1--bundle_1_1.internal.relationship.get'],
|
||||
['jsonapi.entity_type_1--bundle_1_1.internal.relationship.post'],
|
||||
['jsonapi.entity_type_1--bundle_1_1.internal.relationship.patch'],
|
||||
['jsonapi.entity_type_1--bundle_1_1.internal.relationship.delete'],
|
||||
['jsonapi.entity_type_1--bundle_1_1.external.related'],
|
||||
['jsonapi.entity_type_1--bundle_1_1.external.relationship.get'],
|
||||
['jsonapi.entity_type_1--bundle_1_1.external.relationship.post'],
|
||||
['jsonapi.entity_type_1--bundle_1_1.external.relationship.patch'],
|
||||
['jsonapi.entity_type_1--bundle_1_1.external.relationship.delete'],
|
||||
['jsonapi.entity_type_1--bundle_1_1.both.related'],
|
||||
['jsonapi.entity_type_1--bundle_1_1.both.relationship.get'],
|
||||
['jsonapi.entity_type_1--bundle_1_1.both.relationship.post'],
|
||||
['jsonapi.entity_type_1--bundle_1_1.both.relationship.patch'],
|
||||
['jsonapi.entity_type_1--bundle_1_1.both.relationship.delete'],
|
||||
['jsonapi.resource_list'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that no routes are created for internal resources.
|
||||
*
|
||||
* @dataProvider notExpectedRoutes
|
||||
*/
|
||||
public function testInternalRoutes($route): void {
|
||||
$this->assertArrayNotHasKey($route, $this->routes['ok']->routes()->all());
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists routes which should have been created.
|
||||
*/
|
||||
public static function notExpectedRoutes() {
|
||||
return [
|
||||
['jsonapi.entity_type_2--bundle_2_1.individual'],
|
||||
['jsonapi.entity_type_2--bundle_2_1.collection'],
|
||||
['jsonapi.entity_type_2--bundle_2_1.collection.post'],
|
||||
['jsonapi.entity_type_2--bundle_2_1.internal.related'],
|
||||
['jsonapi.entity_type_2--bundle_2_1.internal.relationship'],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user