clean install

This commit is contained in:
2024-12-10 15:08:16 +01:00
commit e14eb2d8fd
31193 changed files with 3555714 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
<!--?xml version="1.0" encoding="UTF-8"?-->
<phpunit colors="true">
<testsuites>
<testsuite name="jsonapi_extras">
<directory>./src/</directory>
<directory>../modules/jsonapi_defaults/tests/src/</directory>
</testsuite>
</testsuites>
<!-- Filter for coverage reports. -->
<filter>
<blacklist>
<directory>./vendor</directory>
</blacklist>
<whitelist>
<directory>../src</directory>
</whitelist>
</filter>
</phpunit>

View File

@@ -0,0 +1,704 @@
<?php
namespace Drupal\Tests\jsonapi_extras\Functional;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Url;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\jsonapi_extras\Entity\JsonapiResourceConfig;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\Tests\jsonapi\Functional\JsonApiFunctionalTestBase;
use Drupal\user\Entity\User;
use Symfony\Component\Routing\Route;
/**
* The test class for the main functionality.
*
* @group jsonapi_extras
*/
class JsonApiExtrasFunctionalTest extends JsonApiFunctionalTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'jsonapi_extras',
'basic_auth',
'jsonapi_test_resource_type_building',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
// Check that the e0ipso/shaper library is available.
if (!class_exists("\\Shaper\\DataAdaptor\\DataAdaptorBase")) {
$this->fail('The e0ipso/shaper library is missing. You can install it with `composer require e0ipso/shaper`.');
}
parent::setUp();
// Add vocabs field to the tags.
$this->createEntityReferenceField(
'taxonomy_term',
'tags',
'vocabs',
'Vocabularies',
'taxonomy_vocabulary',
'default',
[
'target_bundles' => [
'tags' => 'taxonomy_vocabulary',
],
'auto_create' => TRUE,
],
FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
);
FieldStorageConfig::create([
'field_name' => 'field_timestamp',
'entity_type' => 'node',
'type' => 'timestamp',
'settings' => [],
'cardinality' => 1,
])->save();
$field_config = FieldConfig::create([
'field_name' => 'field_timestamp',
'label' => 'Timestamp',
'entity_type' => 'node',
'bundle' => 'article',
'required' => FALSE,
'settings' => [],
'description' => '',
]);
$field_config->save();
FieldStorageConfig::create([
'field_name' => 'field_text_moved',
'entity_type' => 'node',
'type' => 'text',
'settings' => [],
'cardinality' => 1,
])->save();
$field_config = FieldConfig::create([
'field_name' => 'field_text_moved',
'label' => 'Text field',
'entity_type' => 'node',
'bundle' => 'article',
'required' => FALSE,
'settings' => [],
'description' => '',
]);
$field_config->save();
FieldStorageConfig::create([
'field_name' => 'field_text_moved_new',
'entity_type' => 'node',
'type' => 'text',
'settings' => [],
'cardinality' => 1,
])->save();
$field_config = FieldConfig::create([
'field_name' => 'field_text_moved_new',
'label' => 'Text field new',
'entity_type' => 'node',
'bundle' => 'article',
'required' => FALSE,
'settings' => [],
'description' => '',
]);
$field_config->save();
$config = \Drupal::configFactory()->getEditable('jsonapi_extras.settings');
$config->set('path_prefix', 'api');
$config->set('include_count', TRUE);
$config->save(TRUE);
static::overrideResources();
$this->resetAll();
$role = $this->user->get('roles')[0]->entity;
$this->grantPermissions(
$role,
['administer nodes', 'administer site configuration']
);
}
/**
* {@inheritdoc}
*
* Appends the 'application/vnd.api+json' if there's no Accept header.
*/
protected function drupalGet($path, array $options = [], array $headers = []) {
if (empty($headers['Accept']) && empty($headers['accept'])) {
$headers['Accept'] = 'application/vnd.api+json';
}
return parent::drupalGet($path, $options, $headers);
}
/**
* Test overwriting a field with another field.
*/
public function testOverwriteFieldWithOtherField() {
$this->createDefaultContent(1, 1, FALSE, TRUE, static::IS_NOT_MULTILINGUAL);
// 1. Test if moving a field over another doesn't break
$this->nodes[0]->field_text_moved->setValue('field_text_moved_value');
$this->nodes[0]->field_text_moved_new->setValue('field_text_moved_new_value');
$this->nodes[0]->save();
$stringResponse = $this->drupalGet('/api/articles');
$output = Json::decode($stringResponse);
$this->assertEquals($this->nodes[0]->field_text_moved_new->value, $output['data'][0]['attributes']['field_text_moved']['value']);
}
/**
* Test sorting on an overwritten field.
*/
public function testSortOverwrittenField() {
$this->createDefaultContent(2, 1, FALSE, TRUE, static::IS_NOT_MULTILINGUAL);
$this->nodes[0]->field_text_moved->setValue('c');
$this->nodes[0]->field_text_moved_new->setValue('b');
$this->nodes[0]->save();
$this->nodes[1]->field_text_moved->setValue('d');
$this->nodes[1]->field_text_moved_new->setValue('a');
$this->nodes[1]->save();
$stringResponse = $this->drupalGet('/api/articles', ['query' => ['sort' => 'field_text_moved.value']]);
$output = Json::decode($stringResponse);
// Check if order changed as expected.
$this->assertEquals('a', $output['data'][0]['attributes']['field_text_moved']['value']);
$this->assertEquals('b', $output['data'][1]['attributes']['field_text_moved']['value']);
$stringResponse = $this->drupalGet('/api/articles', ['query' => ['sort' => '-field_text_moved.value']]);
$output = Json::decode($stringResponse);
// Check if order changed as expected.
$this->assertEquals('b', $output['data'][0]['attributes']['field_text_moved']['value']);
$this->assertEquals('a', $output['data'][1]['attributes']['field_text_moved']['value']);
}
/**
* Tests that resource type fields can be aliased per resource type.
*
* @see Drupal\jsonapi_test_resource_type_building\EventSubscriber\ResourceTypeBuildEventSubscriber
*
* @todo Create a test similar to this
*/
public function testResourceTypeFieldAliasing() {
/** @var \Drupal\jsonapi_extras\ResourceType\ConfigurableResourceTypeRepository $resourceTypeRepository */
$resourceTypeRepository = $this->container->get('jsonapi.resource_type.repository');
$nodeArticleType = $resourceTypeRepository->getByTypeName('node--article');
$this->assertSame('owner', $nodeArticleType->getPublicName('uid'));
$resource_type_field_aliases = [
'node--article' => [
'uid' => 'author',
],
];
\Drupal::state()->set('jsonapi_test_resource_type_builder.resource_type_field_aliases', $resource_type_field_aliases);
Cache::invalidateTags(['jsonapi_resource_types']);
$this->assertSame('author', $resourceTypeRepository->getByTypeName('node--article')->getPublicName('uid'));
}
/**
* Test the GET method.
*/
public function testRead() {
$num_articles = 61;
$this->createDefaultContent($num_articles, 5, TRUE, TRUE, static::IS_NOT_MULTILINGUAL);
// Make the link for node/3 to point to an entity.
$this->nodes[3]->field_link->setValue(['uri' => 'entity:node/' . $this->nodes[2]->id()]);
$this->nodes[3]->save();
$this->nodes[40]->uid->set(0, 1);
$this->nodes[40]->save();
// 1. Make sure the api root is under '/api' and not '/jsonapi'.
/** @var \Symfony\Component\Routing\RouteCollection $route_collection */
$route_collection = \Drupal::service('router.route_provider')
->getRoutesByPattern('/api');
$this->assertInstanceOf(
Route::class, $route_collection->get('jsonapi.resource_list')
);
$this->drupalGet('/jsonapi');
$this->assertSession()->statusCodeEquals(404);
// 2. Make sure the count is included in collections. This also tests the
// overridden paths.
$output = Json::decode($this->drupalGet('/api/articles'));
$this->assertSame($num_articles, (int) $output['meta']['count']);
$this->assertSession()->statusCodeEquals(200);
// 3. Check disabled resources.
$this->drupalGet('/api/taxonomy_vocabulary/taxonomy_vocabulary');
$this->assertSession()->statusCodeEquals(404);
// 4. Check renamed fields.
$output = Json::decode($this->drupalGet('/api/articles/' . $this->nodes[0]->uuid()));
$this->assertArrayNotHasKey('type', $output['data']['attributes']);
$this->assertArrayHasKey('contentType', $output['data']['relationships']);
$this->assertSame('contentTypes', $output['data']['relationships']['contentType']['data']['type']);
$output = Json::decode($this->drupalGet('/api/contentTypes/' . $this->nodes[0]->type->entity->uuid()));
$this->assertArrayNotHasKey('type', $output['data']['attributes']);
$this->assertSame('article', $output['data']['attributes']['machineName']);
// 5. Check disabled fields.
$output = Json::decode($this->drupalGet('/api/articles/' . $this->nodes[1]->uuid()));
$this->assertArrayNotHasKey('uuid', $output['data']['attributes']);
// 6. Test the field enhancers: DateTimeEnhancer.
$output = Json::decode($this->drupalGet('/api/articles/' . $this->nodes[2]->uuid()));
$timestamp = \DateTime::createFromFormat('Y-m-d\TH:i:sO', $output['data']['attributes']['createdAt'])
->format('U');
$this->assertSame((int) $timestamp, $this->nodes[2]->getCreatedTime());
// 7. Test the field enhancers: UuidLinkEnhancer.
$output = Json::decode($this->drupalGet('/api/articles/' . $this->nodes[3]->uuid()));
$expected_link = 'entity:node/article/' . $this->nodes[2]->uuid();
$this->assertSame($expected_link, $output['data']['attributes']['link']['uri']);
// 8. Test the field enhancers: SingleNestedEnhancer.
$output = Json::decode($this->drupalGet('/api/articles/' . $this->nodes[3]->uuid()));
$this->assertIsString($output['data']['attributes']['body']);
// 9. Test the related endpoint.
// This tests the overridden resource name, the overridden field names and
// the disabled fields.
$output = Json::decode($this->drupalGet('/api/articles/' . $this->nodes[4]->uuid() . '/contentType'));
$this->assertArrayNotHasKey('type', $output['data']['attributes']);
$this->assertSame('article', $output['data']['attributes']['machineName']);
$this->assertSame('contentTypes', $output['data']['type']);
$this->assertArrayNotHasKey('uuid', $output['data']['attributes']);
// 10. Test the relationships endpoint.
$output = Json::decode($this->drupalGet('/api/articles/' . $this->nodes[4]->uuid() . '/relationships/contentType'));
$this->assertSame('contentTypes', $output['data']['type']);
$this->assertArrayHasKey('id', $output['data']);
// 11. Test the related endpoint on a multiple cardinality relationship.
$output = Json::decode($this->drupalGet('/api/articles/' . $this->nodes[5]->uuid() . '/tags'));
$this->assertCount(count($this->nodes[5]->get('field_tags')->getValue()), $output['data']);
$this->assertSame('taxonomy_term--tags', $output['data'][0]['type']);
// 12. Test the relationships endpoint.
$output = Json::decode($this->drupalGet('/api/articles/' . $this->nodes[5]->uuid() . '/relationships/tags'));
$this->assertCount(count($this->nodes[5]->get('field_tags')->getValue()), $output['data']);
$this->assertArrayHasKey('id', $output['data'][0]);
// 13. Test a disabled related resource of single cardinality.
$this->drupalGet('/api/taxonomy_term/tags/' . $this->tags[0]->uuid() . '/vid');
$this->assertSession()->statusCodeEquals(404);
$output = Json::decode($this->drupalGet('/api/taxonomy_term/tags/' . $this->tags[0]->uuid() . '/relationships/vid'));
$this->assertSession()->statusCodeEquals(200);
$this->assertNull($output['data']);
// 14. Test a disabled related resource of multiple cardinality.
$this->tags[1]->vocabs->set(0, 'tags');
$this->tags[1]->save();
$output = Json::decode($this->drupalGet('/api/taxonomy_term/tags/' . $this->tags[0]->uuid() . '/vocabs'));
$this->assertTrue(empty($output['data']));
$output = Json::decode($this->drupalGet('/api/taxonomy_term/tags/' . $this->tags[0]->uuid() . '/relationships/vocabs'));
$this->assertTrue(empty($output['data']));
// 15. Test included resource.
$output = Json::decode($this->drupalGet(
'/api/articles/' . $this->nodes[6]->uuid(),
['query' => ['include' => 'owner']]
));
$this->assertSame('user--user', $output['included'][0]['type']);
// 16. Test disabled included resources.
$output = Json::decode($this->drupalGet(
'/api/taxonomy_term/tags/' . $this->tags[0]->uuid(),
['query' => ['include' => 'vocabs,vid']]
));
$this->assertArrayNotHasKey('included', $output);
// 17. Test nested filters with renamed field.
$output = Json::decode($this->drupalGet(
'/api/articles',
[
'query' => [
'filter' => [
'owner.name' => [
'value' => User::load(1)->getAccountName(),
],
],
],
]
));
// There is only one article for the admin.
$this->assertSame($this->nodes[40]->uuid(), $output['data'][0]['id']);
}
/**
* Test POST/PATCH.
*/
public function testWrite() {
$this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
$this->createDefaultContent(0, 3, FALSE, FALSE, static::IS_NOT_MULTILINGUAL);
// 1. Successful post.
$collection_url = Url::fromRoute('jsonapi.articles.collection');
$body = [
'data' => [
'type' => 'articles',
'attributes' => [
'langcode' => 'en',
'title' => 'My custom title',
'default_langcode' => '1',
'body' => 'Custom value',
'timestamp' => '2017-12-23T08:45:17+0100',
],
'relationships' => [
'contentType' => [
'data' => [
'type' => 'contentTypes',
'id' => NodeType::load('article')->uuid(),
],
],
'owner' => [
'data' => ['type' => 'user--user', 'id' => User::load(1)->uuid()],
],
'tags' => [
'data' => [
['type' => 'taxonomy_term--tags', 'id' => $this->tags[0]->uuid()],
['type' => 'taxonomy_term--tags', 'id' => $this->tags[1]->uuid()],
],
],
],
],
];
$response = $this->request('POST', $collection_url, [
'body' => Json::encode($body),
'auth' => [$this->user->getAccountName(), $this->user->pass_raw],
'headers' => [
'Content-Type' => 'application/vnd.api+json',
'Accept' => 'application/vnd.api+json',
],
]);
$created_response = Json::decode((string) $response->getBody());
$this->assertEquals(201, $response->getStatusCode());
$this->assertArrayHasKey('internalId', $created_response['data']['attributes']);
$this->assertCount(2, $created_response['data']['relationships']['tags']['data']);
$this->assertSame($created_response['data']['links']['self']['href'], $response->getHeader('Location')[0]);
$date = new \DateTime($body['data']['attributes']['timestamp']);
$created_node = Node::load($created_response['data']['attributes']['internalId']);
$this->assertSame((int) $date->format('U'), (int) $created_node->get('field_timestamp')->value);
// 2. Successful relationships PATCH.
$uuid = $created_response['data']['id'];
$relationships_url = Url::fromUserInput('/api/articles/' . $uuid . '/relationships/tags');
$body = [
'data' => [
['type' => 'taxonomy_term--tags', 'id' => $this->tags[2]->uuid()],
],
];
$response = $this->request('POST', $relationships_url, [
'body' => Json::encode($body),
'auth' => [$this->user->getAccountName(), $this->user->pass_raw],
'headers' => ['Content-Type' => 'application/vnd.api+json'],
]);
$created_response = Json::decode((string) $response->getBody());
$this->assertCount(3, $created_response['data']);
}
/**
* Creates the JSON:API Resource Config entities to override the resources.
*/
protected static function overrideResources() {
// Disable the taxonomy_vocabulary resource.
JsonapiResourceConfig::create([
'id' => 'taxonomy_vocabulary--taxonomy_vocabulary',
'disabled' => TRUE,
'path' => 'taxonomy_vocabulary/taxonomy_vocabulary',
'resourceType' => 'taxonomy_vocabulary--taxonomy_vocabulary',
'resourceFields' => [],
])->save();
// Override paths and fields in the articles resource.
JsonapiResourceConfig::create([
'id' => 'node--article',
'disabled' => FALSE,
'path' => 'articles',
'resourceType' => 'articles',
'resourceFields' => [
'field_text_moved' => [
'fieldName' => 'field_text_moved',
'publicName' => 'field_text_moved',
'enhancer' => ['id' => ''],
'disabled' => TRUE,
],
'field_text_moved_new' => [
'fieldName' => 'field_text_moved_new',
'publicName' => 'field_text_moved',
'enhancer' => ['id' => ''],
'disabled' => FALSE,
],
'field_date_sort' => [
'fieldName' => 'field_date_sort',
'publicName' => 'field_date_sort',
'enhancer' => ['id' => ''],
'disabled' => TRUE,
],
'field_date_sort_new' => [
'fieldName' => 'field_date_sort_new',
'publicName' => 'field_date_sort',
'enhancer' => ['id' => ''],
'disabled' => FALSE,
],
'nid' => [
'fieldName' => 'nid',
'publicName' => 'internalId',
'enhancer' => ['id' => ''],
'disabled' => FALSE,
],
'uuid' => [
'fieldName' => 'uuid',
'publicName' => 'uuid',
'enhancer' => ['id' => ''],
'disabled' => TRUE,
],
'vid' => [
'fieldName' => 'vid',
'publicName' => 'vid',
'enhancer' => ['id' => ''],
'disabled' => TRUE,
],
'langcode' => [
'fieldName' => 'langcode',
'publicName' => 'langcode',
'enhancer' => ['id' => ''],
'disabled' => TRUE,
],
'type' => [
'fieldName' => 'type',
'publicName' => 'contentType',
'enhancer' => ['id' => ''],
'disabled' => FALSE,
],
'status' => [
'fieldName' => 'status',
'publicName' => 'isPublished',
'enhancer' => ['id' => ''],
'disabled' => FALSE,
],
'title' => [
'fieldName' => 'title',
'publicName' => 'title',
'enhancer' => ['id' => ''],
'disabled' => FALSE,
],
'uid' => [
'fieldName' => 'uid',
'publicName' => 'owner',
'enhancer' => ['id' => ''],
'disabled' => FALSE,
],
'created' => [
'fieldName' => 'created',
'publicName' => 'createdAt',
'enhancer' => [
'id' => 'date_time',
'settings' => ['dateTimeFormat' => 'Y-m-d\TH:i:sO'],
],
'disabled' => FALSE,
],
'changed' => [
'fieldName' => 'changed',
'publicName' => 'updatedAt',
'enhancer' => [
'id' => 'date_time',
'settings' => ['dateTimeFormat' => 'Y-m-d\TH:i:sO'],
],
'disabled' => FALSE,
],
'promote' => [
'fieldName' => 'promote',
'publicName' => 'isPromoted',
'enhancer' => ['id' => ''],
'disabled' => FALSE,
],
'sticky' => [
'fieldName' => 'sticky',
'publicName' => 'sticky',
'enhancer' => ['id' => ''],
'disabled' => TRUE,
],
'revision_timestamp' => [
'fieldName' => 'revision_timestamp',
'publicName' => 'revision_timestamp',
'enhancer' => ['id' => ''],
'disabled' => TRUE,
],
'revision_uid' => [
'fieldName' => 'revision_uid',
'publicName' => 'revision_uid',
'enhancer' => ['id' => ''],
'disabled' => TRUE,
],
'revision_log' => [
'fieldName' => 'revision_log',
'publicName' => 'revision_log',
'enhancer' => ['id' => ''],
'disabled' => TRUE,
],
'revision_translation_affected' => [
'fieldName' => 'revision_translation_affected',
'publicName' => 'revision_translation_affected',
'enhancer' => ['id' => ''],
'disabled' => TRUE,
],
'default_langcode' => [
'fieldName' => 'default_langcode',
'publicName' => 'default_langcode',
'enhancer' => ['id' => ''],
'disabled' => TRUE,
],
'path' => [
'fieldName' => 'path',
'publicName' => 'path',
'enhancer' => ['id' => ''],
'disabled' => FALSE,
],
'body' => [
'fieldName' => 'body',
'publicName' => 'body',
'enhancer' => ['id' => 'nested', 'settings' => ['path' => 'value']],
'disabled' => FALSE,
],
'field_link' => [
'fieldName' => 'field_link',
'publicName' => 'link',
'enhancer' => ['id' => 'uuid_link'],
'disabled' => FALSE,
],
'field_timestamp' => [
'fieldName' => 'field_timestamp',
'publicName' => 'timestamp',
'enhancer' => [
'id' => 'date_time',
'settings' => ['dateTimeFormat' => 'Y-m-d\TH:i:sO'],
],
'disabled' => FALSE,
],
'comment' => [
'fieldName' => 'comment',
'publicName' => 'comment',
'enhancer' => ['id' => ''],
'disabled' => FALSE,
],
'field_image' => [
'fieldName' => 'field_image',
'publicName' => 'image',
'enhancer' => ['id' => ''],
'disabled' => FALSE,
],
'field_recipes' => [
'fieldName' => 'field_recipes',
'publicName' => 'recipes',
'enhancer' => ['id' => ''],
'disabled' => FALSE,
],
'field_tags' => [
'fieldName' => 'field_tags',
'publicName' => 'tags',
'enhancer' => ['id' => ''],
'disabled' => FALSE,
],
],
])->save();
// Override the resource type in the node_type resource.
JsonapiResourceConfig::create([
'id' => 'node_type--node_type',
'disabled' => FALSE,
'path' => 'contentTypes',
'resourceType' => 'contentTypes',
'resourceFields' => [
'type' => [
'fieldName' => 'type',
'publicName' => 'machineName',
'enhancer' => ['id' => ''],
'disabled' => FALSE,
],
'status' => [
'fieldName' => 'status',
'publicName' => 'isEnabled',
'enhancer' => ['id' => ''],
'disabled' => FALSE,
],
'langcode' => [
'fieldName' => 'langcode',
'publicName' => 'langcode',
'enhancer' => ['id' => ''],
'disabled' => TRUE,
],
'uuid' => [
'fieldName' => 'uuid',
'publicName' => 'uuid',
'enhancer' => ['id' => ''],
'disabled' => TRUE,
],
],
])->save();
}
/**
* Test disabled resource show in admin list.
*/
public function testDisabledResourcesShowInAdminList() {
$vocabulary = Vocabulary::create([
'name' => $this->randomMachineName(),
'vid' => mb_strtolower($this->randomMachineName()),
]);
$vocabulary->save();
$admin_user = $this->createUser([], 'test_admin_user', TRUE);
$this->drupalLogin($admin_user);
$this->drupalGet('/admin/config/services/jsonapi/resource_types');
$row = $this->assertSession()->elementExists('css', sprintf('#jsonapi-enabled-resources-list table tr:contains("%s")', 'taxonomy_term--' . $vocabulary->id()));
$this->assertSession()->elementExists('named', ['link', 'Overwrite'], $row);
$this->drupalGet('/admin/config/services/jsonapi/add/resource_types/taxonomy_term/' . $vocabulary->id());
$this->getSession()->getPage()->checkField('edit-disabled');
$this->getSession()->getPage()->pressButton('edit-submit');
$row = $this->assertSession()->elementExists('css', sprintf('#jsonapi-disabled-resources-list table tr:contains("%s")', 'taxonomy_term--' . $vocabulary->id()));
$this->assertSession()->elementExists('named', ['link', 'Revert'], $row);
// Add another vocabulary.
$vocabulary2 = Vocabulary::create([
'name' => $this->randomMachineName(),
'vid' => mb_strtolower($this->randomMachineName()),
]);
$vocabulary2->save();
// Set the default to disabled.
\Drupal::configFactory()->getEditable('jsonapi_extras.settings')->set('default_disabled', TRUE)->save();
Cache::invalidateTags(['jsonapi_resource_types']);
$this->drupalGet('/admin/config/services/jsonapi/resource_types');
$row = $this->assertSession()->elementExists('css', sprintf('#jsonapi-disabled-resources-list table tr:contains("%s")', 'taxonomy_term--' . $vocabulary2->id()));
$this->assertSession()->elementExists('named', ['link', 'Enable'], $row);
// Test that internal entity-types can't be enabled.
\Drupal::service('module_installer')->install(['entity_test']);
$bundle_info = \Drupal::service('entity_type.bundle.info')->getBundleInfo('entity_test_no_label');
$this->drupalGet('/admin/config/services/jsonapi/resource_types');
foreach (array_keys($bundle_info) as $bundle) {
$this->assertSession()->elementNotContains('css', '#jsonapi-disabled-resources-list', 'entity_test_no_label--' . $bundle);
}
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Drupal\Tests\jsonapi_extras\Functional;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\Tests\jsonapi\Functional\JsonApiFunctionalTestBase;
/**
* Provides helper methods for the JSON API Extras module's functional tests.
*
* @internal
*/
abstract class JsonApiExtrasFunctionalTestBase extends JsonApiFunctionalTestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Add vocabs field to the tags.
$this->createEntityReferenceField(
'taxonomy_term',
'tags',
'vocabs',
'Vocabularies',
'taxonomy_vocabulary',
'default',
[
'target_bundles' => [
'tags' => 'taxonomy_vocabulary',
],
'auto_create' => TRUE,
],
FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
);
FieldStorageConfig::create([
'field_name' => 'field_timestamp',
'entity_type' => 'node',
'type' => 'timestamp',
'settings' => [],
'cardinality' => 1,
])->save();
$field_config = FieldConfig::create([
'field_name' => 'field_timestamp',
'label' => 'Timestamp',
'entity_type' => 'node',
'bundle' => 'article',
'required' => FALSE,
'settings' => [],
'description' => '',
]);
$field_config->save();
$config = \Drupal::configFactory()->getEditable('jsonapi_extras.settings');
$config->set('path_prefix', 'api');
$config->set('include_count', TRUE);
$config->save(TRUE);
static::overrideResources();
$this->resetAll();
$role = $this->user->get('roles')[0]->entity;
$this->grantPermissions(
$role,
['administer nodes', 'administer site configuration']
);
}
/**
* {@inheritdoc}
*
* Appends the 'application/vnd.api+json' if there's no Accept header.
*/
protected function drupalGet($path, array $options = [], array $headers = []) {
if (empty($headers['Accept']) && empty($headers['accept'])) {
$headers['Accept'] = 'application/vnd.api+json';
}
return parent::drupalGet($path, $options, $headers);
}
/**
* Creates the JSON API Resource Config entities to override the resources.
*/
protected static function overrideResources() {}
}

View File

@@ -0,0 +1,252 @@
<?php
namespace Drupal\Tests\jsonapi_extras\Kernel\Controller;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Config\ConfigException;
use Drupal\jsonapi\Access\EntityAccessChecker;
use Drupal\jsonapi\Controller\EntityResource;
use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel as JsonApiDocumentTopLevel2;
use Drupal\jsonapi\JsonApiResource\ResourceObject;
use Drupal\jsonapi\ResourceType\ResourceType;
use Drupal\jsonapi\ResourceType\ResourceTypeAttribute;
use Drupal\KernelTests\KernelTestBase;
use Drupal\node\Entity\NodeType;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* @coversDefaultClass \Drupal\jsonapi\Controller\EntityResource
* @covers \Drupal\jsonapi_extras\Normalizer\ConfigEntityDenormalizer
* @group jsonapi_extras
* @group legacy
*
* @internal
*/
class EntityResourceTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'file',
'node',
'jsonapi',
'serialization',
'system',
'user',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
NodeType::create([
'type' => 'article',
])->save();
Role::create([
'id' => RoleInterface::ANONYMOUS_ID,
])->save();
}
/**
* @covers ::createIndividual
*/
public function testCreateIndividualConfig() {
Role::load(Role::ANONYMOUS_ID)
->grantPermission('administer content types')
->save();
$resource_type = new ResourceType('node_type', 'node_type', NodeType::class);
$resource_type->setRelatableResourceTypes([]);
$payload = Json::encode([
'data' => [
'type' => 'node--test',
'attributes' => [
'type' => 'test',
'name' => 'Test Type',
'description' => 'Lorem ipsum',
],
],
]);
$entity_resource = new EntityResource(
$this->container->get('entity_type.manager'),
$this->container->get('entity_field.manager'),
$this->container->get('jsonapi.resource_type.repository'),
$this->container->get('renderer'),
$this->container->get('entity.repository'),
$this->container->get('jsonapi.include_resolver'),
new EntityAccessChecker(
$this->container->get('jsonapi.resource_type.repository'),
$this->container->get('router.no_access_checks'),
$this->container->get('current_user'),
$this->container->get('entity.repository')
),
$this->container->get('jsonapi.field_resolver'),
$this->container->get('jsonapi.serializer'),
$this->container->get('datetime.time'),
$this->container->get('current_user')
);
$response = $entity_resource->createIndividual($resource_type, Request::create('/jsonapi/node_type/node_type', 'POST', [], [], [], [], $payload));
// As a side effect, the node type will also be saved.
$node_type = NodeType::load('test');
$this->assertInstanceOf(JsonApiDocumentTopLevel2::class, $response->getResponseData());
$data = $response->getResponseData()->getData()->getIterator()->offsetGet(0);
$this->assertInstanceOf(ResourceObject::class, $data);
$this->assertEquals($node_type->uuid(), $data->getId());
$this->assertEquals(201, $response->getStatusCode());
}
/**
* @covers ::patchIndividual
* @dataProvider patchIndividualConfigProvider
*/
public function testPatchIndividualConfig($values) {
// List of fields to be ignored.
$ignored_fields = ['uuid', 'entityTypeId', 'type'];
$node_type = NodeType::create([
'type' => 'test',
'name' => 'Test Type',
'description' => '',
]);
$node_type->save();
$parsed_node_type = NodeType::create($values);
Role::load(Role::ANONYMOUS_ID)
->grantPermission('administer content types')
->save();
Role::load(Role::ANONYMOUS_ID)
->grantPermission('edit any article content')
->save();
$payload = Json::encode([
'data' => [
'type' => 'node_type--node_type',
'id' => $node_type->uuid(),
'attributes' => $values,
],
]);
$request = Request::create('/jsonapi/node/node_type/' . $node_type->uuid(), 'PATCH', [], [], [], [], $payload);
$resource_type = new ResourceType('node_type', 'node_type', NodeType::class, FALSE, TRUE, TRUE, FALSE, [
'type' => new ResourceTypeAttribute('drupal_internal__type'),
]);
$resource_type->setRelatableResourceTypes([]);
$entity_resource = new EntityResource(
$this->container->get('entity_type.manager'),
$this->container->get('entity_field.manager'),
$this->container->get('jsonapi.resource_type.repository'),
$this->container->get('renderer'),
$this->container->get('entity.repository'),
$this->container->get('jsonapi.include_resolver'),
new EntityAccessChecker(
$this->container->get('jsonapi.resource_type.repository'),
$this->container->get('router.no_access_checks'),
$this->container->get('current_user'),
$this->container->get('entity.repository')
),
$this->container->get('jsonapi.field_resolver'),
$this->container->get('jsonapi.serializer'),
$this->container->get('datetime.time'),
$this->container->get('current_user')
);
$response = $entity_resource->patchIndividual($resource_type, $node_type, $request);
// As a side effect, the node will also be saved.
$this->assertInstanceOf(JsonApiDocumentTopLevel2::class, $response->getResponseData());
$updated_node_type = $response->getResponseData()->getData()->getIterator()->offsetGet(0);
$this->assertInstanceOf(ResourceObject::class, $updated_node_type);
// If the field is ignored then we should not see a difference.
foreach ($values as $field_name => $value) {
in_array($field_name, $ignored_fields) ?
$this->assertNotSame($value, $node_type->get($field_name)) :
$this->assertSame($value, $node_type->get($field_name));
}
$this->assertEquals(200, $response->getStatusCode());
}
/**
* Provides data for the testPatchIndividualConfig.
*
* @return array
* The input data for the test function.
*/
public static function patchIndividualConfigProvider(): array {
return [
[['description' => 'PATCHED', 'status' => FALSE]],
[[]],
];
}
/**
* @covers ::patchIndividual
* @dataProvider patchIndividualConfigFailedProvider
*/
public function testPatchIndividualFailedConfig($values, $expected_message) {
$this->expectException(ConfigException::class);
$this->expectExceptionMessage($expected_message);
$this->testPatchIndividualConfig($values);
}
/**
* Provides data for the testPatchIndividualFailedConfig.
*
* @return array
* The input data for the test function.
*/
public static function patchIndividualConfigFailedProvider(): array {
return [
[
['type' => 'article', 'status' => FALSE],
"The machine name of the 'Content type' bundle cannot be changed.",
],
];
}
/**
* @covers ::deleteIndividual
*/
public function testDeleteIndividualConfig() {
$node_type = NodeType::create([
'type' => 'test',
'name' => 'Test Type',
'description' => 'Lorem ipsum',
]);
$id = $node_type->id();
$node_type->save();
Role::load(Role::ANONYMOUS_ID)
->grantPermission('administer content types')
->save();
$entity_resource = new EntityResource(
$this->container->get('entity_type.manager'),
$this->container->get('entity_field.manager'),
$this->container->get('jsonapi.resource_type.repository'),
$this->container->get('renderer'),
$this->container->get('entity.repository'),
$this->container->get('jsonapi.include_resolver'),
new EntityAccessChecker(
$this->container->get('jsonapi.resource_type.repository'),
$this->container->get('router.no_access_checks'),
$this->container->get('current_user'),
$this->container->get('entity.repository')
),
$this->container->get('jsonapi.field_resolver'),
$this->container->get('jsonapi.serializer'),
$this->container->get('datetime.time'),
$this->container->get('current_user')
);
$response = $entity_resource->deleteIndividual($node_type);
// As a side effect, the node will also be deleted.
$count = $this->container->get('entity_type.manager')
->getStorage('node_type')
->getQuery()
->condition('type', $id)
->count()
->execute();
$this->assertEquals(0, $count);
$this->assertNull($response->getResponseData());
$this->assertEquals(204, $response->getStatusCode());
}
}

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\jsonapi_extras\Kernel;
use Drupal\Core\Cache\Cache;
use Drupal\jsonapi_extras\Entity\JsonapiResourceConfig;
use Drupal\jsonapi_extras\ResourceType\ConfigurableResourceTypeRepository;
use Drupal\KernelTests\KernelTestBase;
/**
* Defines a test for the 'default_disabled' setting on resource configs.
*
* @group jsonapi_extras
*/
class DefaultDisabledResourceConfigTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'file',
'jsonapi',
'jsonapi_extras',
'entity_test',
'serialization',
'user',
'system',
'text',
'field',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('entity_test');
}
/**
* Tests the default disabled setting.
*/
public function testDefaultDisabled(): void {
$resource_repository = $this->container->get('jsonapi.resource_type.repository');
assert($resource_repository instanceof ConfigurableResourceTypeRepository);
$resource = $resource_repository->get('entity_test', 'entity_test');
$this->assertFalse($resource->isInternal());
$this->config('jsonapi_extras.settings')->set('default_disabled', TRUE)->save();
$resource_repository->reset();
Cache::invalidateTags(['jsonapi_resource_types']);
$resource = $resource_repository->get('entity_test', 'entity_test');
$this->assertTrue($resource->isInternal());
JsonapiResourceConfig::create([
'id' => 'entity_test--entity_test',
'disabled' => FALSE,
'path' => 'entity_test/entity_test',
'resourceType' => 'entity_test--entity_test',
'resourceFields' => [],
])->save();
Cache::invalidateTags(['jsonapi_resource_types']);
$resource = $resource_repository->get('entity_test', 'entity_test');
$this->assertFalse($resource->isInternal());
}
}

View File

@@ -0,0 +1,361 @@
<?php
namespace Drupal\Tests\jsonapi_extras\Kernel;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\file\Entity\File;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\Tests\image\Kernel\ImageFieldCreationTrait;
use Drupal\Tests\jsonapi\Kernel\JsonapiKernelTestBase;
use Drupal\user\Entity\Role;
use Drupal\user\Entity\User;
use Drupal\user\RoleInterface;
/**
* @coversDefaultClass \Drupal\jsonapi_extras\EntityToJsonApi
* @group jsonapi
* @group jsonapi_serializer
* @group legacy
*
* @internal
*/
class EntityToJsonApiTest extends JsonapiKernelTestBase {
use ImageFieldCreationTrait;
/**
* System under test.
*
* @var \Drupal\jsonapi_extras\EntityToJsonApi
*/
protected $sut;
/**
* {@inheritdoc}
*/
protected static $modules = [
'jsonapi',
'jsonapi_extras',
'field',
'node',
'serialization',
'system',
'taxonomy',
'text',
'user',
'file',
'image',
];
/**
* Node type.
*
* @var \Drupal\node\Entity\NodeType
*/
private $nodeType;
/**
* Vocabulary.
*
* @var \Drupal\taxonomy\Entity\Vocabulary
*/
private $vocabulary;
/**
* Node.
*
* @var \Drupal\node\Entity\Node
*/
private $node;
/**
* A user instance for the test.
*
* @var \Drupal\user\UserInterface
*/
protected $user;
/**
* A user instance for the test.
*
* @var \Drupal\user\UserInterface
*/
protected $user2;
/**
* Taxonomy term.
*
* @var \Drupal\taxonomy\TermInterface
*/
protected $term1;
/**
* Taxonomy term.
*
* @var \Drupal\taxonomy\TermInterface
*/
protected $term2;
/**
* File entity.
*
* @var \Drupal\file\FileInterface
*/
protected $file;
/**
* Role.
*
* @var \Drupal\user\RoleInterface
*/
protected $role;
/**
* {@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('system', ['sequences']);
$this->installSchema('node', ['node_access']);
$this->installSchema('user', ['users_data']);
$this->installSchema('file', ['file_usage']);
$this->nodeType = NodeType::create([
'type' => 'article',
]);
$this->nodeType->save();
$this->createEntityReferenceField(
'node',
'article',
'field_tags',
'Tags',
'taxonomy_term',
'default',
['target_bundles' => ['tags']],
FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
);
// @todo Remove this once we drop support for Drupal 9.
if (version_compare(\Drupal::VERSION, '10.3.0', '>=')) {
$this->createImageField('field_image', 'node', 'article');
}
else {
$this->createImageField('field_image', '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' => 1,
'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');
$this->role = Role::create([
'id' => RoleInterface::ANONYMOUS_ID,
'permissions' => [
'access content',
],
]);
$this->role->save();
$this->sut = $this->container->get('jsonapi_extras.entity.to_jsonapi');
}
/**
* @covers ::serialize
* @covers ::normalize
*/
public function testSerialize() {
$entities = [
[
$this->node,
['field_tags'],
[
[
'type' => 'taxonomy_term--tags',
'id' => $this->term1->uuid(),
'attributes' => [
'drupal_internal__tid' => (int) $this->term1->id(),
'name' => $this->term1->label(),
],
],
[
'type' => 'taxonomy_term--tags',
'id' => $this->term2->uuid(),
'attributes' => [
'drupal_internal__tid' => (int) $this->term2->id(),
'name' => $this->term2->label(),
],
],
],
],
[$this->user, [], []],
[$this->file, [], []],
[$this->term1, [], []],
// Make sure we also support configuration entities.
[$this->nodeType, [], []],
];
array_walk(
$entities,
function ($data) {
[$entity, $include_fields, $expected_includes] = $data;
$this->assertEntity($entity, $include_fields, $expected_includes);
}
);
}
/**
* Test if the request doesn't linger on the request stack.
*
* @see https://www.drupal.org/project/jsonapi_extras/issues/3135950
* @see https://www.drupal.org/project/jsonapi_extras/issues/3124805
*/
public function testRequestStack() {
/** @var \Symfony\Component\HttpFoundation\RequestStack $request_stack */
$request_stack = $this->container->get('request_stack');
$this->sut->serialize($this->node);
$request = $request_stack->pop();
$this->assertNotEquals($request->getPathInfo(), '/jsonapi/node/' . $this->nodeType->id() . '/' . $this->node->uuid(), 'The request from jsonapi_extras.entity.to_jsonapi should not linger in the request stack.');
// The session is gone, set the container to null so teardown doesn't fail.
$this->container = NULL;
}
/**
* Checks entity's serialization/normalization.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to serialize/normalize.
* @param string[] $include_fields
* The list of fields to include.
* @param array[] $expected_includes
* The list of partial structures of the "included" key.
*/
protected function assertEntity(EntityInterface $entity, array $include_fields = [], array $expected_includes = []) {
$output = $this->sut->serialize($entity, $include_fields);
$this->assertTrue(is_string($output));
$this->assertJsonApi(Json::decode($output));
$output = $this->sut->normalize($entity, $include_fields);
$this->assertTrue(is_array($output));
$this->assertJsonApi($output);
// Check the includes if they were passed.
if (!empty($include_fields)) {
$this->assertJsonApiIncludes($output, $expected_includes);
}
}
/**
* Helper to assert if a string is valid JSON:API.
*
* @param array $structured
* The JSON:API data to check.
*/
protected function assertJsonApi(array $structured) {
static::assertNotEmpty($structured['data']['type']);
static::assertNotEmpty($structured['data']['id']);
static::assertNotEmpty($structured['data']['attributes']);
$this->assertTrue(is_string($structured['data']['links']['self']['href']));
}
/**
* Shallowly checks the list of includes.
*
* @param array $structured
* The JSON:API data to check.
* @param array[] $includes
* The list of partial structures of the "included" key.
*/
protected function assertJsonApiIncludes(array $structured, array $includes) {
static::assertFalse(
empty($structured['included']),
'The list of includes should is empty.'
);
foreach ($includes as $i => $include) {
static::assertFalse(
empty($structured['included'][$i]),
sprintf('The include #%d does not exist.', $i)
);
static::assertSame(
$include['type'],
$structured['included'][$i]['type'],
sprintf('The type of include #%d does not match expected value.', $i)
);
foreach ($include['attributes'] as $attribute => $expected_value) {
static::assertSame(
$expected_value,
$structured['included'][$i]['attributes'][$attribute],
sprintf(
'The "%s" of include #%d doest match the expected value.',
$attribute,
$i
)
);
}
}
}
}

View File

@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\jsonapi_extras\Kernel\Plugin\jsonapi\FieldEnhancer;
use Drupal\jsonapi_extras\Plugin\jsonapi\FieldEnhancer\ListFieldEnhancer;
use Drupal\KernelTests\KernelTestBase;
use Drupal\options\Plugin\Field\FieldType\ListIntegerItem;
use Shaper\Util\Context;
/**
* Tests ListFieldEnhancer.
*
* @group jsonapi_extras
*/
final class ListFieldEnhancerTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'file',
'jsonapi_extras',
'jsonapi',
'serialization',
'system',
'user',
'options',
];
/**
* Tests JSON schema output.
*/
public function testGetOutputJsonSchema(): void {
self::assertEquals([
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'value' => [
'anyOf' => [
['type' => 'string'],
['type' => 'number'],
['type' => 'null'],
],
],
'label' => [
'anOf' => [
['type' => 'string'],
['type' => 'null'],
],
],
],
],
], $this->getInstance()->getOutputJsonSchema());
}
/**
* Tests transform.
*/
public function testTransform(): void {
$output = $this->getInstance()->transform([['value' => 1]]);
self::assertEquals([1], $output);
$output = $this->getInstance()->transform('foo');
self::assertEquals('foo', $output);
}
/**
* Tests undo transform.
*/
public function testUndoTransform(): void {
$field_item = $this->createMock(ListIntegerItem::class);
$field_item->expects($this->once())
->method('getPossibleOptions')
->willReturn([1 => 'One', 2 => 'Two', 3 => 'Three']);
$context = new Context();
$context['field_item_object'] = $field_item;
$output = $this->getInstance()->undoTransform([3, 1, 2], $context);
self::assertEquals(
[
['value' => 3, 'label' => 'Three'],
['value' => 1, 'label' => 'One'],
['value' => 2, 'label' => 'Two'],
],
$output
);
}
/**
* Gets a plugin instance for testing.
*
* @return \Drupal\jsonapi_extras\Plugin\jsonapi\FieldEnhancer\ListFieldEnhancer
* The plugin instance.
*/
private function getInstance(): ListFieldEnhancer {
return $this->container->get('plugin.manager.resource_field_enhancer')->createInstance('list');
}
}