external_entities-8.x-2.x-dev/tests/src/Functional/SimpleExternalEntityTest.php
tests/src/Functional/SimpleExternalEntityTest.php
<?php
namespace Drupal\Tests\external_entities\Functional;
use Behat\Mink\Exception\ElementNotFoundException;
use Drupal\Component\Serialization\Json;
use Drupal\filter\Entity\FilterFormat;
/**
* Tests creation of a simple external entity.
*
* @group ExternalEntities
*/
class SimpleExternalEntityTest extends ExternalEntitiesBrowserTestBase {
/**
* Authorization token for the REST API.
*
* @var string
*/
const AUTHORIZATION_TOKEN = 'ABCDEF123456789';
/**
* A user with administration permissions.
*
* @var \Drupal\user\UserInterface
*/
protected $account;
/**
* The entity storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $storage;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
global $base_url;
$this->storage = $this->container->get('entity_type.manager')->getStorage('external_entity_type');
// Setup datasets.
$xntt_json_controller = $this
->container
->get('controller_resolver')
->getControllerFromDefinition(
'\Drupal\external_entities_test\Controller\ExternalEntitiesJsonController::listItems'
)[0];
$xntt_json_controller->setRawData('simple', [
'2596b1ba-43bb-4440-9f0c-f1974f733336' => [
'id' => '2596b1ba-43bb-4440-9f0c-f1974f733336',
'title' => 'Simple title 1',
'short_text' => 'Just a short string',
'rich_text' => '<h2>Some HTML tags</h2>',
'rich_text_2' => '<h2>Other HTML tags</h2>',
'status' => TRUE,
'multival' => [
'key1' => [
'val' => 'abc',
'something' => 'ignored',
],
'key2' => [
'none' => 'def',
],
'key3' => [
'val' => 'ghi',
],
],
// Note: test environment as a different timezone.
'first_date' => '2025-07-21T20:15:13.512Z',
'second_date' => '1754062029',
'refs' => [
'ref2',
'ref3',
],
'unmapped' => 'value not mapped',
],
'2596b1ba-43bb-4440-9f0c-f1974f733337' => [
'id' => '2596b1ba-43bb-4440-9f0c-f1974f733337',
'title' => 'Simple title 2',
'short_text' => 'Just another short string',
'status' => FALSE,
],
]);
$xntt_json_controller->setRawData('ref', [
'ref1' => [
'id' => 'ref1',
'label' => 'Term 1',
],
'ref2' => [
'id' => 'ref2',
'label' => 'Term 2',
],
'ref3' => [
'id' => 'ref3',
'label' => 'Term 3',
],
'ref4' => [
'id' => 'ref4',
'label' => 'Term 4',
],
]);
// Enables authentication by adding an authentication token.
$xntt_json_controller->addToken(static::AUTHORIZATION_TOKEN);
// Setup reference external entity type.
/** @var \Drupal\external_entities\Entity\ExternalEntityType $ref */
$ref = $this->storage->create([
'id' => 'ref',
'label' => 'Ref',
'label_plural' => 'Refs',
'base_path' => 'ref',
'description' => '',
'read_only' => FALSE,
'debug_level' => 0,
'field_mappers' => [],
'storage_clients' => [],
'data_aggregator' => [],
'persistent_cache_max_age' => 0,
]);
// Sets aggregator.
$ref->setDataAggregatorId('single')->setDataAggregatorConfig([
'storage_clients' => [
[
'id' => 'rest',
'config' => [
'endpoint' => $base_url . '/external-entities-test/ref',
'endpoint_options' => [
'single' => '',
'count' => '',
'count_mode' => NULL,
'cache' => FALSE,
'limit_qcount' => 0,
'limit_qtime' => 0,
],
'response_format' => 'json',
'data_path' => [
'list' => '',
'single' => '',
'keyed_by_id' => FALSE,
'count' => '',
],
'pager' => [
'default_limit' => 50,
'type' => 'pagination',
'page_parameter' => 'page',
'page_parameter_type' => 'pagenum',
'page_start_one' => FALSE,
'page_size_parameter' => 'pageSize',
'page_size_parameter_type' => 'pagesize',
],
'api_key' => [
'type' => 'none',
'header_name' => '',
'key' => '',
],
'http' => [
'headers' => '',
],
'parameters' => [
'list' => [],
'list_param_mode' => 'query',
'single' => [],
'single_param_mode' => 'query',
],
'filtering' => [
'drupal' => TRUE,
'basic' => FALSE,
'basic_fields' => [],
'list_support' => 'none',
'list_join' => '',
],
],
],
],
]);
// We need to save here to have base fields mappable.
$ref->save();
// ID field mapping.
$ref->setFieldMapperId('id', 'generic');
$ref->setFieldMapperConfig(
'id',
[
'property_mappings' => [
'value' => [
'id' => 'direct',
'config' => [
'mapping' => 'id',
'required_field' => TRUE,
'main_property' => TRUE,
'data_processors' => [
[],
],
],
],
],
'debug_level' => 0,
]
);
// UUID field mapping.
$ref->setFieldMapperId('uuid', 'generic');
$ref->setFieldMapperConfig(
'uuid',
[
'property_mappings' => [
'value' => [
'id' => 'direct',
'config' => [
'mapping' => 'id',
'required_field' => FALSE,
'main_property' => TRUE,
'data_processors' => [
[
'id' => 'default',
'config' => [],
],
],
],
],
],
'debug_level' => 0,
]
);
// Title field mapping.
$ref->setFieldMapperId('title', 'generic');
$ref->setFieldMapperConfig(
'title',
[
'property_mappings' => [
'value' => [
'id' => 'direct',
'config' => [
'mapping' => 'label',
'required_field' => TRUE,
'main_property' => TRUE,
'data_processors' => [],
],
],
],
'debug_level' => 0,
]
);
$ref->save();
// Create a new filter format for next formatted text fields.
$full_html_format = FilterFormat::create([
'format' => 'full_html',
'name' => 'Full HTML',
'weight' => 1,
'filters' => [],
]);
$full_html_format->save();
// Setup tested simple external entity type.
/** @var \Drupal\external_entities\Entity\ExternalEntityType $type */
$type = $this->storage->create([
'id' => 'simple_external_entity',
'label' => 'Simple external entity',
'label_plural' => 'Simple external entities',
'base_path' => 'simple-external-entity',
'description' => '',
'read_only' => FALSE,
'debug_level' => 0,
'field_mappers' => [],
'storage_clients' => [],
'data_aggregator' => [],
'persistent_cache_max_age' => 0,
]);
// Sets aggregator.
$type->setDataAggregatorId('single')->setDataAggregatorConfig([
'storage_clients' => [
[
'id' => 'rest',
'config' => [
'endpoint' => $base_url . '/external-entities-test/simple',
'endpoint_options' => [
'single' => '',
'count' => '',
'count_mode' => NULL,
'cache' => FALSE,
'limit_qcount' => 0,
'limit_qtime' => 0,
],
'response_format' => 'json',
'data_path' => [
'list' => '',
'single' => '',
'keyed_by_id' => FALSE,
'count' => '',
],
'pager' => [
'default_limit' => 50,
'type' => 'pagination',
'page_parameter' => 'page',
'page_parameter_type' => 'pagenum',
'page_start_one' => FALSE,
'page_size_parameter' => 'pageSize',
'page_size_parameter_type' => 'pagesize',
],
'api_key' => [
'type' => 'none',
'header_name' => '',
'key' => '',
],
'http' => [
'headers' => '',
],
'parameters' => [
'list' => [],
'list_param_mode' => 'query',
'single' => [],
'single_param_mode' => 'query',
],
'filtering' => [
'drupal' => TRUE,
'basic' => FALSE,
'basic_fields' => [],
'list_support' => 'none',
'list_join' => '',
],
],
],
],
]);
$type->save();
// Add fields.
$this
->createField('simple_external_entity', 'plain_text', 'string')
->createField('simple_external_entity', 'fixed_string', 'string')
->createField('simple_external_entity', 'a_boolean', 'boolean')
->createField('simple_external_entity', 'a_rich_text', 'text', ['settings' => ['allowed_formats' => ['full_html']]])
->createField('simple_external_entity', 'a_plain_text', 'text', ['settings' => ['allowed_formats' => ['plain_text']]])
->createField('simple_external_entity', 'multi_string', 'string', ['multiple' => TRUE])
->createField('simple_external_entity', 'a_date', 'datetime', ['settings' => ['datetime_type' => 'datetime']])
->createField('simple_external_entity', 'a_timestamp', 'timestamp')
->createReferenceField('simple_external_entity', 'ref', 'ref', NULL, ['multiple' => TRUE]);
// Set field mappers...
// ID field mapping.
$type->setFieldMapperId('id', 'generic');
$type->setFieldMapperConfig(
'id',
[
'property_mappings' => [
'value' => [
'id' => 'direct',
'config' => [
'mapping' => 'id',
'required_field' => TRUE,
'main_property' => TRUE,
'data_processors' => [
[
'id' => 'default',
'config' => [],
],
],
],
],
],
'debug_level' => 0,
]
);
// UUID field mapping.
$type->setFieldMapperId('uuid', 'generic');
$type->setFieldMapperConfig(
'uuid',
[
'property_mappings' => [
'value' => [
'id' => 'direct',
'config' => [
'mapping' => 'id',
'required_field' => FALSE,
'main_property' => TRUE,
'data_processors' => [
[
'id' => 'default',
'config' => [],
],
],
],
],
],
'debug_level' => 0,
]
);
// Title field mapping.
$type->setFieldMapperId('title', 'generic');
$type->setFieldMapperConfig(
'title',
[
'property_mappings' => [
'value' => [
'id' => 'direct',
'config' => [
'mapping' => 'title',
'required_field' => TRUE,
'main_property' => TRUE,
'data_processors' => [
[
'id' => 'default',
'config' => [],
],
],
],
],
],
'debug_level' => 0,
]
);
// Plain text field mapping.
$type->setFieldMapperId('plain_text', 'generic');
$type->setFieldMapperConfig(
'plain_text',
[
'property_mappings' => [
'value' => [
'id' => 'simple',
'config' => [
'mapping' => 'short_text',
'required_field' => FALSE,
'main_property' => TRUE,
'data_processors' => [
[
'id' => 'default',
'config' => [],
],
],
],
],
],
'debug_level' => 0,
]
);
// Fixed string field mapping.
$type->setFieldMapperId('fixed_string', 'generic');
$type->setFieldMapperConfig(
'fixed_string',
[
'property_mappings' => [
'value' => [
'id' => 'constant',
'config' => [
'mapping' => 'A fixed string',
'required_field' => FALSE,
'main_property' => TRUE,
],
],
],
'debug_level' => 0,
]
);
// Multi-string field mapping.
$type->setFieldMapperId('multi_string', 'generic');
$type->setFieldMapperConfig(
'multi_string',
[
'property_mappings' => [
'value' => [
'id' => 'simple',
'config' => [
'mapping' => 'multival.*.val',
'required_field' => FALSE,
'main_property' => TRUE,
'data_processors' => [
[
'id' => 'default',
'config' => [],
],
],
],
],
],
'debug_level' => 0,
]
);
// Rich text field mapping.
$type->setFieldMapperId('a_rich_text', 'text');
$type->setFieldMapperConfig(
'a_rich_text',
[
'property_mappings' => [
'value' => [
'id' => 'simple',
'config' => [
'mapping' => 'rich_text',
'required_field' => FALSE,
'main_property' => TRUE,
'data_processors' => [
[
'id' => 'default',
'config' => [],
],
],
],
],
],
'format' => 'full_html',
'debug_level' => 0,
]
);
// Formatted plain text field mapping.
$type->setFieldMapperId('a_plain_text', 'text');
$type->setFieldMapperConfig(
'a_plain_text',
[
'property_mappings' => [
'value' => [
'id' => 'simple',
'config' => [
'mapping' => 'rich_text_2',
'required_field' => FALSE,
'main_property' => TRUE,
'data_processors' => [
[
'id' => 'default',
'config' => [],
],
],
],
],
],
'format' => 'plain_text',
'debug_level' => 0,
]
);
// Boolean field mapping.
$type->setFieldMapperId('a_boolean', 'generic');
$type->setFieldMapperConfig(
'a_boolean',
[
'property_mappings' => [
'value' => [
'id' => 'simple',
'config' => [
'mapping' => 'status',
'required_field' => FALSE,
'main_property' => TRUE,
'data_processors' => [
[
'id' => 'boolean',
'config' => [],
],
],
],
],
],
'debug_level' => 0,
]
);
// Date+time field mapping.
$type->setFieldMapperId('a_date', 'generic');
$type->setFieldMapperConfig(
'a_date',
[
'property_mappings' => [
'value' => [
'id' => 'simple',
'config' => [
'mapping' => 'first_date',
'required_field' => FALSE,
'main_property' => TRUE,
'data_processors' => [
[
'id' => 'datetime',
'config' => [
'source_format' => 'iso8601',
'drupal_format' => 'timestamp',
],
],
],
],
],
],
'debug_level' => 0,
]
);
// Timestamp field mapping.
$type->setFieldMapperId('a_timestamp', 'generic');
$type->setFieldMapperConfig(
'a_timestamp',
[
'property_mappings' => [
'value' => [
'id' => 'simple',
'config' => [
'mapping' => 'second_date',
'required_field' => FALSE,
'main_property' => TRUE,
'data_processors' => [
[
'id' => 'datetime',
'config' => [
'source_format' => 'timestamp',
'drupal_format' => 'timestamp',
],
],
],
],
],
],
'debug_level' => 0,
]
);
// Entity reference field mapping.
$type->setFieldMapperId('ref', 'generic');
$type->setFieldMapperConfig(
'ref',
[
'property_mappings' => [
'target_id' => [
'id' => 'simple',
'config' => [
'mapping' => 'refs.*',
'required_field' => FALSE,
'main_property' => TRUE,
'data_processors' => [],
],
],
],
'debug_level' => 0,
]
);
$type->save();
// Create the user with all needed permissions.
$this->account = $this->drupalCreateUser([
'manage external entity test datasets',
'administer external entity types',
'view simple_external_entity external entity',
'update simple_external_entity external entity',
'delete simple_external_entity external entity',
'view simple_external_entity external entity collection',
'create simple_external_entity external entity',
'view ref external entity',
'update ref external entity',
'delete ref external entity',
'create ref external entity',
'view ref external entity collection',
'use text format full_html',
'access site reports',
]);
$this->drupalLogin($this->account);
}
/**
* Tests creation of a rule and then triggering its execution.
*/
public function testSimpleExternalEntity() {
/** @var \Drupal\Tests\WebAssert $assert */
$assert = $this->assertSession();
$this->drupalGet('admin/structure/external-entity-types');
$assert->pageTextContains('Simple external entity');
// Check "ref test endpoint".
$ref_json = Json::decode($this->drupalGet('external-entities-test/ref'));
$this
->assertSession()
->statusCodeEquals(200);
$this
->assertCount(4, $ref_json);
$this
->assertSession()
->responseHeaderEquals('Content-Type', 'application/json');
// Check "simple test endpoint".
$simple_json = Json::decode($this->drupalGet('external-entities-test/simple'));
$this
->assertSession()
->statusCodeEquals(200);
$this
->assertCount(2, $simple_json);
$this
->assertSession()
->responseHeaderEquals('Content-Type', 'application/json');
// Entity list check.
$this->drupalGet('simple-external-entity');
$assert->pageTextContainsOnce('Simple title 1');
$assert->pageTextContainsOnce('Simple title 2');
// Entity 1 test.
$this->drupalGet('simple-external-entity/2596b1ba-43bb-4440-9f0c-f1974f733336');
$assert->pageTextContains('Simple title 1');
$assert->pageTextContainsOnce('Just a short string');
$assert->pageTextContainsOnce('A fixed string');
$assert->pageTextContainsOnce('abc');
$assert->pageTextNotContains('def');
$assert->pageTextContainsOnce('ghi');
// Note: due to default test environment timezone, we have a different day.
$assert->pageTextContainsOnce('Tue, 22 Jul 2025 - 06:15');
$assert->pageTextContainsOnce('Sat, 2 Aug 2025 - 01:27');
$assert->pageTextNotContains('Term 1');
$assert->pageTextContainsOnce('Term 2');
$assert->pageTextContainsOnce('Term 3');
$assert->responseContains('<div>On</div>');
$assert->pageTextContainsOnce('Some HTML tags');
$assert->pageTextNotContains('<h2>Some HTML tags</h2>');
$assert->pageTextContainsOnce('<h2>Other HTML tags</h2>');
$this->drupalGet('simple-external-entity/2596b1ba-43bb-4440-9f0c-f1974f733337');
$assert->pageTextContains('Simple title 2');
$assert->pageTextContainsOnce('Just another short string');
$assert->pageTextContainsOnce('A fixed string');
$assert->responseContains('<div>Off</div>');
// Edit test.
// - Without permission, no editing.
$this->drupalGet('simple-external-entity/2596b1ba-43bb-4440-9f0c-f1974f733336/edit');
// Change title.
$this->fillField('Title', 'Updated title 1');
$this->pressButton('Save');
// We don't test if 'Updated title 1' is present because it will be there!
// While the remote data has not been edited, the local "in memory" entity
// has been updated with the new title and the message issued to tell the
// entity was updated contains the updated title (messenger service). So
// the updated title will appear because of the messenger service.
$assert->pageTextContains('Simple title 1');
// - Allow edit access.
$xntt_type = $this->storage->load('simple_external_entity');
$agg_config = $xntt_type->getDataAggregatorConfig();
$agg_config['storage_clients'][0]['config']['api_key'] = [
'type' => 'bearer',
'header_name' => 'Authorization',
'key' => 'Bearer ' . static::AUTHORIZATION_TOKEN,
];
$xntt_type->setDataAggregatorConfig($agg_config)->save();
$this->drupalGet('simple-external-entity/2596b1ba-43bb-4440-9f0c-f1974f733336/edit');
// Change title.
$this->fillField('Title', 'Updated title 1');
// Uncheck 'a_boolean'.
$this->fillField('a_boolean', FALSE);
// Change date.
$this->fillField('edit-a-date-0-value-date', '2025-07-27');
$this->fillField('edit-a-date-0-value-time', '07:42:00');
// Change timestamp.
$this->fillField('edit-a-timestamp-0-value-date', '2025-07-28');
$this->fillField('edit-a-timestamp-0-value-time', '08:06:00');
// Remove Term 2.
try {
// Drupal 10 use a "Remove" button.
$this->pressButton('ref_0_remove_button');
}
catch (ElementNotFoundException $e) {
// Drupal 9 uses autocomplete field.
$this->fillField('ref[0][target_id]', '');
}
$this->pressButton('Save');
$assert->pageTextContains('Updated title 1');
$assert->pageTextContainsOnce('Just a short string');
$assert->pageTextContainsOnce('A fixed string');
$assert->pageTextContainsOnce('Sat, 26 Jul 2025 - 11:42');
$assert->pageTextContainsOnce('Mon, 28 Jul 2025 - 08:06');
$assert->pageTextNotContains('Term 1');
$assert->pageTextNotContains('Term 2');
$assert->pageTextContainsOnce('Term 3');
$assert->responseContains('<div>Off</div>');
// Special case: we should exepct to only have 'abc' and 'ghi' one time but
// we will have 2 times each because the saving method will keep old values
// and override with new ones. Old values in the multival array are stored
// using text keys while the new one will be added using numeric keys.
// That's how we come with 2 times the same value from the external entity
// side: one form the original keys ('key1' and 'key3') and one from the
// new saved values using key '0' and '1'.
// If we wanted to avoid that, we should use a different field mapper that
// would be aware of the original keys and remap values correctly.
$assert->pageTextContains('abc');
$assert->pageTextNotContains('def');
$assert->pageTextContains('ghi');
// Check stored results.
$simple_json = Json::decode($this->drupalGet('external-entities-test/simple/2596b1ba-43bb-4440-9f0c-f1974f733336'));
$this->assertEquals('value not mapped', $simple_json['unmapped'], 'Preserve unmapped properties');
$this->assertEquals('2025-07-26T11:42:00', $simple_json['first_date'], 'Preserve ISO format');
$this->assertEquals('1753653960', $simple_json['second_date'], 'Preserve timestamp format');
$this->assertEquals(0, $simple_json['status'], 'Save boolean');
}
}
