entity_mesh-1.1.1/tests/src/Kernel/EntityMeshProcessingModeTest.php
tests/src/Kernel/EntityMeshProcessingModeTest.php
<?php
namespace Drupal\Tests\entity_mesh\Kernel;
use Drupal\entity_mesh\TrackerInterface;
use Drupal\filter\Entity\FilterFormat;
use Drupal\node\Entity\Node;
/**
* Tests the synchronous/asynchronous processing mode functionality.
*
* @group entity_mesh
*/
class EntityMeshProcessingModeTest extends EntityMeshTestBase {
/**
* The entity mesh render service.
*
* @var \Drupal\entity_mesh\EntityRender
*/
protected $entityMeshRender;
/**
* The queue factory service.
*
* @var \Drupal\Core\Queue\QueueFactory
*/
protected $queueFactory;
/**
* The queue for entity mesh.
*
* @var \Drupal\Core\Queue\QueueInterface
*/
protected $queue;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Get services - these should be available after parent::setUp().
$this->entityMeshRender = $this->container->get('entity_mesh.entity_render');
$this->queueFactory = $this->container->get('queue');
$this->queue = $this->queueFactory->get('entity_mesh_queue_worker');
// Create a content type if it doesn't exist.
$types = $this->container->get('entity_type.manager')
->getStorage('node_type')
->loadMultiple();
if (!isset($types['article'])) {
$this->createContentType(['type' => 'article', 'name' => 'Article']);
}
// The parent class already creates basic_html, but ensure other formats
// exist.
$additional_formats = ['plain_text', 'full_html'];
foreach ($additional_formats as $format_id) {
if (!FilterFormat::load($format_id)) {
FilterFormat::create([
'format' => $format_id,
'name' => ucfirst(str_replace('_', ' ', $format_id)),
'weight' => 0,
'filters' => [],
])->save();
}
}
// Clear any cached entity_mesh data.
$this->container->get('cache.default')->deleteAll();
// Ensure hooks are discovered.
$this->container->get('module_handler')->resetImplementations();
}
/**
* {@inheritdoc}
*/
protected function createExampleNodes() {
// This test doesn't use the standard test nodes from the base class.
// We create specific nodes for each test method.
}
/**
* {@inheritdoc}
*/
public static function linkCasesProvider() {
// This test doesn't use the data provider pattern from the base class.
// Return minimal data to satisfy the parent method signature.
return [[
'source_entity_id' => 1,
'source_entity_langcode' => 'en',
'target_href' => '/test',
'expected_target_link_type' => 'internal',
],
];
}
/**
* Override parent testLinks to prevent it from running.
*
* This test is skipped because this test class focuses on processing modes.
*/
public function testLinks(
$source_entity_id = NULL,
$source_entity_langcode = NULL,
$target_href = NULL,
$expected_target_link_type = NULL,
$expected_target_subcategory = NULL,
$expected_target_title = NULL,
$expected_target_scheme = NULL,
$expected_target_host = NULL,
$expected_target_entity_type = NULL,
$expected_target_entity_bundle = NULL,
$expected_target_entity_id = NULL,
$expected_target_entity_langcode = NULL,
$expected_source_entity_type = NULL,
$expected_source_entity_bundle = NULL,
$expected_source_entity_langcode = NULL,
$expected_source_title = NULL,
$source_should_exist = TRUE,
) {
// This test class doesn't use the parent's testLinks method.
$this->markTestSkipped('This test is not applicable to processing mode tests.');
}
/**
* Tests the countEntityLinks method.
*/
public function testCountEntityLinks() {
// Create a node with no links.
$node = Node::create([
'type' => 'article',
'title' => 'Test node without links',
'body' => [
'value' => 'This is a test node with no links.',
'format' => 'plain_text',
],
]);
$node->save();
// Note: The node might have automatic links (like "Read more") added by
// the theme. So we'll store the baseline count.
$baseline_count = $this->entityMeshRender->countEntityLinks($node);
$this->assertGreaterThanOrEqual(0, $baseline_count, 'Link count should be non-negative.');
// Create a node with regular links.
$node_with_links = Node::create([
'type' => 'article',
'title' => 'Test node with links',
'body' => [
'value' => 'This node has <a href="/node/100">one link</a> and <a href="/node/200">another link</a> and <a href="https://example.com">external link</a>.',
'format' => 'basic_html',
],
]);
$node_with_links->save();
$count = $this->entityMeshRender->countEntityLinks($node_with_links);
// The actual count should include any theme-added links plus our 3
// explicit links.
$this->assertGreaterThanOrEqual(3, $count, 'Node should have at least 3 links (our explicit links).');
// Create a node with links and iframes.
$node_with_mixed = Node::create([
'type' => 'article',
'title' => 'Test node with mixed content',
'body' => [
'value' => 'This has <a href="/node/100">a link</a> and <iframe src="/media/200"></iframe> and <iframe src="/node/300"></iframe>.',
'format' => 'full_html',
],
]);
$node_with_mixed->save();
$count = $this->entityMeshRender->countEntityLinks($node_with_mixed);
$this->assertGreaterThanOrEqual(3, $count, 'Node should have at least 3 links (1 link + 2 iframes).');
// Test with duplicate links (should be counted as unique).
$node_with_duplicates = Node::create([
'type' => 'article',
'title' => 'Test node with duplicate links',
'body' => [
'value' => 'This has <a href="/node/100">link 1</a> and <a href="/node/100">same link</a> and <a href="/node/200">link 2</a>.',
'format' => 'basic_html',
],
]);
$node_with_duplicates->save();
$count = $this->entityMeshRender->countEntityLinks($node_with_duplicates);
// Should have baseline + 2 unique links (duplicate /node/1 counted once)
$this->assertGreaterThanOrEqual(2, $count, 'Should have at least 2 unique links.');
}
/**
* Tests synchronous processing mode with entities below the limit.
*/
public function testSynchronousProcessingBelowLimit() {
// Set configuration for synchronous mode with limit of 5.
$config = $this->config('entity_mesh.settings');
$config->set('processing_mode', 'synchronous');
$config->set('synchronous_limit', 5);
$config->save();
// Clear the tracker table before testing.
\Drupal::database()->truncate('entity_mesh_tracker')->execute();
// Create target nodes first so internal links resolve properly.
$target1 = Node::create([
'type' => 'article',
'title' => 'Target node 1',
'body' => ['value' => 'Target 1', 'format' => 'plain_text'],
]);
$target1->save();
$target2 = Node::create([
'type' => 'article',
'title' => 'Target node 2',
'body' => ['value' => 'Target 2', 'format' => 'plain_text'],
]);
$target2->save();
// Create a node with 3 links (below the limit).
$node = Node::create([
'type' => 'article',
'title' => 'Test node with few links',
'body' => [
'value' => 'This has <a href="/node/' . $target1->id() . '">link 1</a> and <a href="/node/' . $target2->id() . '">link 2</a> and <a href="https://example.com">external link</a>.',
'format' => 'basic_html',
],
]);
$node->save();
// The node should be processed synchronously and tracked with PROCESSED
// status.
$tracker_entry = \Drupal::database()->select('entity_mesh_tracker', 't')
->fields('t')
->condition('entity_type', 'node')
->condition('entity_id', $node->id())
->execute()
->fetchObject();
$this->assertNotFalse($tracker_entry, 'Node processed synchronously should be tracked.');
$this->assertEquals(
TrackerInterface::STATUS_PROCESSED,
$tracker_entry->status,
'Node should have PROCESSED status after synchronous processing.'
);
// Verify the entity mesh data was saved.
$query = \Drupal::database()->select('entity_mesh', 'em')
->fields('em')
->condition('source_entity_type', 'node')
->condition('source_entity_id', $node->id());
$results = $query->execute()->fetchAll();
// We should have at least 3 links (could be more if theme adds links)
$this->assertGreaterThanOrEqual(3, count($results), 'At least three targets should be saved in entity_mesh table.');
}
/**
* Tests synchronous processing mode with entities above the limit.
*/
public function testSynchronousProcessingAboveLimit() {
// Set configuration for synchronous mode with limit of 3.
$config = $this->config('entity_mesh.settings');
$config->set('processing_mode', 'synchronous');
$config->set('synchronous_limit', 3);
$config->save();
// Clear the tracker table before testing.
\Drupal::database()->truncate('entity_mesh_tracker')->execute();
// Create target nodes.
for ($i = 1; $i <= 5; $i++) {
$target = Node::create([
'type' => 'article',
'title' => "Target node $i",
'body' => ['value' => "Target $i", 'format' => 'plain_text'],
]);
$target->save();
}
// Create a node with 5 links (above the limit).
$node = Node::create([
'type' => 'article',
'title' => 'Test node with many links',
'body' => [
'value' => 'Links: <a href="/node/1">1</a> <a href="/node/2">2</a> <a href="/node/3">3</a> <a href="/node/4">4</a> <a href="/node/5">5</a>.',
'format' => 'basic_html',
],
]);
$node->save();
// The node should be added to tracker because it has more links than the
// limit.
$tracker_count = \Drupal::database()->select('entity_mesh_tracker', 't')
->condition('entity_type', 'node')
->condition('entity_id', $node->id())
->countQuery()
->execute()
->fetchField();
$this->assertEquals(1, $tracker_count, 'Node with links above limit should be tracked.');
// Verify the tracker entry has the correct status.
$tracker_entry = \Drupal::database()->select('entity_mesh_tracker', 't')
->fields('t')
->condition('entity_type', 'node')
->condition('entity_id', $node->id())
->execute()
->fetchObject();
$this->assertNotFalse($tracker_entry, 'Tracker entry should exist.');
$this->assertEquals(
TrackerInterface::STATUS_PENDING,
$tracker_entry->status,
'Tracker status should be PENDING.'
);
$this->assertEquals(
TrackerInterface::OPERATION_PROCESS,
$tracker_entry->operation,
'Tracker operation should be PROCESS.'
);
}
/**
* Tests asynchronous processing mode.
*/
public function testAsynchronousProcessing() {
// Set configuration for asynchronous mode.
$config = $this->config('entity_mesh.settings');
$config->set('processing_mode', 'asynchronous');
$config->save();
// Clear the tracker table before testing.
\Drupal::database()->truncate('entity_mesh_tracker')->execute();
// Create a node with any number of links.
$node = Node::create([
'type' => 'article',
'title' => 'Test node for async',
'body' => [
'value' => 'This has <a href="/node/1">just one link</a>.',
'format' => 'basic_html',
],
]);
$node->save();
// In async mode, all nodes should be tracked regardless of link count.
$tracker_count = \Drupal::database()->select('entity_mesh_tracker', 't')
->condition('entity_type', 'node')
->condition('entity_id', $node->id())
->countQuery()
->execute()
->fetchField();
$this->assertEquals(1, $tracker_count, 'In async mode, all nodes should be tracked.');
// Verify no data was saved synchronously.
$query = \Drupal::database()->select('entity_mesh', 'em')
->fields('em')
->condition('source_entity_type', 'node')
->condition('source_entity_id', $node->id());
$results = $query->execute()->fetchAll();
$this->assertCount(0, $results, 'No targets should be saved synchronously in async mode.');
}
/**
* Tests synchronous mode with exactly the limit number of links.
*/
public function testSynchronousProcessingExactLimit() {
// Set configuration for synchronous mode with limit of 3.
$config = $this->config('entity_mesh.settings');
$config->set('processing_mode', 'synchronous');
$config->set('synchronous_limit', 3);
$config->save();
// Clear the tracker table before testing.
\Drupal::database()->truncate('entity_mesh_tracker')->execute();
// Create target nodes first.
for ($i = 1; $i <= 3; $i++) {
$target = Node::create([
'type' => 'article',
'title' => "Target node $i",
'body' => ['value' => "Target $i", 'format' => 'plain_text'],
]);
$target->save();
}
// Create a node with exactly 3 links (equal to the limit).
$node = Node::create([
'type' => 'article',
'title' => 'Test node with exact limit',
'body' => [
'value' => 'Links: <a href="/node/1">1</a> <a href="/node/2">2</a> <a href="/node/3">3</a>.',
'format' => 'basic_html',
],
]);
$node->save();
// Debug: check the actual link count - may have extra theme links.
$link_count = $this->entityMeshRender->countEntityLinks($node);
// If there are more than 3 links (due to theme additions), the node will
// be tracked.
if ($link_count > 3) {
// The node should be tracked because it has more links than the limit.
$this->assertGreaterThan(3, $link_count, 'Node has more than 3 links due to theme additions.');
$tracker_count = \Drupal::database()->select('entity_mesh_tracker', 't')
->condition('entity_type', 'node')
->condition('entity_id', $node->id())
->countQuery()
->execute()
->fetchField();
$this->assertEquals(1, $tracker_count, 'Node with more links than limit should be tracked.');
// Skip the rest of the test.
return;
}
// The node should be processed synchronously and tracked with PROCESSED
// status when equal to limit.
$tracker_entry = \Drupal::database()->select('entity_mesh_tracker', 't')
->fields('t')
->condition('entity_type', 'node')
->condition('entity_id', $node->id())
->execute()
->fetchObject();
$this->assertNotFalse($tracker_entry, 'Node with links equal to limit should be tracked.');
$this->assertEquals(
TrackerInterface::STATUS_PROCESSED,
$tracker_entry->status,
'Node should have PROCESSED status after synchronous processing.'
);
// Verify the entity mesh data was saved.
$query = \Drupal::database()->select('entity_mesh', 'em')
->fields('em')
->condition('source_entity_type', 'node')
->condition('source_entity_id', $node->id());
$results = $query->execute()->fetchAll();
$this->assertGreaterThanOrEqual(3, count($results), 'At least three targets should be saved in entity_mesh table.');
}
/**
* Tests that asynchronous node deletion adds entry to tracker table.
*
* When a node is deleted in asynchronous mode, it should be added to the
* entity_mesh_tracker table with operation=DELETE and status=PENDING.
*/
public function testAsynchronousNodeDeletion() {
// Set configuration for asynchronous mode.
$config = $this->config('entity_mesh.settings');
$config->set('processing_mode', 'asynchronous');
$config->save();
// Clear the tracker table before testing.
\Drupal::database()->truncate('entity_mesh_tracker')->execute();
// Create a node.
$node = Node::create([
'type' => 'article',
'title' => 'Test node to delete',
'body' => [
'value' => 'This node will be deleted to test tracker behavior.',
'format' => 'plain_text',
],
]);
$node->save();
$node_id = $node->id();
// Verify the node was tracked for processing on creation.
$tracker_count = \Drupal::database()->select('entity_mesh_tracker', 't')
->condition('entity_type', 'node')
->condition('entity_id', $node_id)
->condition('operation', TrackerInterface::OPERATION_PROCESS)
->countQuery()
->execute()
->fetchField();
$this->assertEquals(1, $tracker_count, 'Node creation should add entry to tracker in async mode.');
// Clear the tracker to focus on deletion behavior.
\Drupal::database()->truncate('entity_mesh_tracker')->execute();
// Delete the node.
$node->delete();
// Verify that the deletion was tracked.
$tracker_entry = \Drupal::database()->select('entity_mesh_tracker', 't')
->fields('t')
->condition('entity_type', 'node')
->condition('entity_id', $node_id)
->execute()
->fetchObject();
$this->assertNotFalse($tracker_entry, 'Node deletion should add entry to tracker table.');
$this->assertEquals(
TrackerInterface::OPERATION_DELETE,
$tracker_entry->operation,
'Tracker entry should have OPERATION_DELETE.'
);
$this->assertEquals(
TrackerInterface::STATUS_PENDING,
$tracker_entry->status,
'Tracker entry should have STATUS_PENDING.'
);
$this->assertEquals('node', $tracker_entry->entity_type, 'Entity type should be node.');
$this->assertEquals($node_id, $tracker_entry->entity_id, 'Entity ID should match deleted node.');
$this->assertNotEmpty($tracker_entry->timestamp, 'Timestamp should be set.');
$this->assertEquals(0, $tracker_entry->retry_count, 'Retry count should be 0 initially.');
}
}
