og-8.x-1.x-dev/tests/src/Kernel/Views/OgGroupRelationshipsViewTest.php
tests/src/Kernel/Views/OgGroupRelationshipsViewTest.php
<?php
declare(strict_types=1);
namespace Drupal\Tests\og\Kernel\Views;
use Drupal\Tests\views\Kernel\ViewsKernelTestBase;
use Drupal\views\Tests\ViewTestData;
use Drupal\views\Views;
use Drupal\node\Entity\NodeType;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
use Drupal\og\Og;
use Drupal\og\OgGroupAudienceHelperInterface;
/**
* Tests the OG Views relationships that aggregate across audience fields.
*
* @group og
*/
class OgGroupRelationshipsViewTest extends ViewsKernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'user',
'field',
'node',
'views',
'og',
'og_test',
'options',
];
/**
* Views used by this test.
*
* @var list<string>
*
* @phpcsSuppress SlevomatCodingStandard.TypeHints.PropertyTypeHint
*/
public static $testViews = [
'test_og_group_content_to_group',
'test_og_group_to_group_content',
];
/**
* Machine name of the group bundle used for groups in this test.
*/
protected string $groupBundle;
/**
* Machine name of the content bundle used for group content.
*/
protected string $contentBundle;
/**
* First group entity used in the fixtures.
*/
protected NodeInterface $groupA;
/**
* Second group entity used in the fixtures.
*/
protected NodeInterface $groupB;
/**
* Group content entity that references the two groups.
*/
protected NodeInterface $contentNode;
/**
* Field name of the first OG audience reference field.
*/
protected string $audienceField1;
/**
* Field name of the second OG audience reference field.
*/
protected string $audienceField2;
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = FALSE): void {
parent::setUp($import_test_views);
// Register the test views from og_test module.
ViewTestData::createTestViews(static::class, ['og_test']);
}
/**
* {@inheritdoc}
*/
protected function setUpFixtures() {
// Install OG config and required entity schemas.
$this->installConfig(['og']);
$this->installConfig(['og_test']);
$this->installEntitySchema('og_membership');
$this->installEntitySchema('user');
$this->installEntitySchema('node');
// Create a group bundle and designate it as an OG group.
$this->groupBundle = mb_strtolower($this->randomMachineName());
NodeType::create([
'type' => $this->groupBundle,
'name' => $this->randomString(),
])->save();
Og::groupTypeManager()->addGroup('node', $this->groupBundle);
// Create a content bundle.
$this->contentBundle = mb_strtolower($this->randomMachineName());
NodeType::create([
'type' => $this->contentBundle,
'name' => $this->randomString(),
])->save();
// Add two distinct OG audience fields to the content bundle.
$this->audienceField1 = mb_strtolower($this->randomMachineName());
$this->audienceField2 = mb_strtolower($this->randomMachineName());
Og::createField(OgGroupAudienceHelperInterface::DEFAULT_FIELD, 'node', $this->contentBundle, ['field_name' => $this->audienceField1]);
Og::createField(OgGroupAudienceHelperInterface::DEFAULT_FIELD, 'node', $this->contentBundle, ['field_name' => $this->audienceField2]);
// Create two groups.
$this->groupA = Node::create([
'type' => $this->groupBundle,
'title' => $this->randomString(),
]);
$this->groupA->save();
$this->groupB = Node::create([
'type' => $this->groupBundle,
'title' => $this->randomString(),
]);
$this->groupB->save();
// Create one content node that references two groups via the two audience
// fields.
$this->contentNode = Node::create([
'type' => $this->contentBundle,
'title' => $this->randomString(),
$this->audienceField1 => [
['target_id' => (int) $this->groupA->id()],
],
$this->audienceField2 => [
['target_id' => (int) $this->groupB->id()],
],
]);
$this->contentNode->save();
parent::setUpFixtures();
}
/**
* Verifies group content → group relationship duplicates per audience ref.
*/
public function testGroupContentToGroupRelationship(): void {
$view = Views::getView('test_og_group_content_to_group');
// Filter the base to the content bundle so we only consider our content
// node.
$view->setDisplay('default');
$view->display_handler->setOption('filters', [
'type' => [
'id' => 'type',
'table' => 'node_field_data',
'field' => 'type',
'value' => [$this->contentBundle => $this->contentBundle],
'entity_type' => 'node',
'entity_field' => 'type',
'plugin_id' => 'bundle',
],
]);
$this->executeView($view);
// Expect two rows for the single content, one per related group across the
// two different audience fields.
$this->assertCount(2, $view->result, 'Content appears once per audience relationship.');
$nids = array_map(fn($row) => (int) $row->_entity->id(), $view->result);
$this->assertEquals([$this->contentNode->id(), $this->contentNode->id()], $nids, 'Both rows are the same content nid.');
}
/**
* Verifies group → group content relationship returns the two groups.
*/
public function testGroupToGroupContentRelationship(): void {
$view = Views::getView('test_og_group_to_group_content');
// Filter the base to the group bundle so the base rows are groups.
$view->setDisplay('default');
$view->display_handler->setOption('filters', [
'type' => [
'id' => 'type',
'table' => 'node_field_data',
'field' => 'type',
'value' => [$this->groupBundle => $this->groupBundle],
'entity_type' => 'node',
'entity_field' => 'type',
'plugin_id' => 'bundle',
],
]);
$this->executeView($view);
// Each of the two groups is related to the single content entity via a
// different audience field, so we expect both groups to appear.
$this->assertCount(2, $view->result, 'Both groups appear once.');
$nids = array_map(fn($row) => (int) $row->_entity->id(), $view->result);
sort($nids);
$expected = [
(int) $this->groupA->id(),
(int) $this->groupB->id(),
];
sort($expected);
$this->assertEquals($expected, $nids, 'The two groups are returned.');
}
/**
* Verifies no duplicates when both audience fields reference the same group.
*/
public function testNoDuplicateWhenSameGroupAcrossMultipleFields(): void {
// Create an additional content node that references the same group in
// both audience fields. The UNION mapping should deduplicate the pair
// (entity_id, group_id) so the joined result has a single row.
$content_node = Node::create([
'type' => $this->contentBundle,
'title' => $this->randomString(),
$this->audienceField1 => [
['target_id' => (int) $this->groupA->id()],
],
$this->audienceField2 => [
['target_id' => (int) $this->groupA->id()],
],
]);
$content_node->save();
$view = Views::getView('test_og_group_content_to_group');
$view->setDisplay('default');
// Filter to only this content node using both type and nid filters.
$view->display_handler->setOption('filters', [
'type' => [
'id' => 'type',
'table' => 'node_field_data',
'field' => 'type',
'value' => [$this->contentBundle => $this->contentBundle],
'entity_type' => 'node',
'entity_field' => 'type',
'plugin_id' => 'bundle',
],
'nid' => [
'id' => 'nid',
'table' => 'node_field_data',
'field' => 'nid',
'value' => ['value' => (string) $content_node->id()],
'plugin_id' => 'numeric',
'operator' => '=',
],
]);
$this->executeView($view);
// Expect a single row because both audience fields reference the same
// group, and the UNION mapping removes duplicates.
$this->assertCount(1, $view->result, 'Content appears once when both audience fields reference the same group.');
$nids = array_map(fn($row) => (int) $row->_entity->id(), $view->result);
$this->assertEquals([(int) $content_node->id()], $nids, 'The content nid appears once.');
}
/**
* Verifies LEFT vs INNER join behavior via the relationship's required flag.
*/
public function testRelationshipRequiredVsOptional(): void {
// Create a second content bundle with no audience fields.
$lonely_bundle = mb_strtolower($this->randomMachineName());
NodeType::create([
'type' => $lonely_bundle,
'name' => $this->randomString(),
])->save();
// Create one node in that bundle (no audience references).
$lonely = Node::create([
'type' => $lonely_bundle,
'title' => $this->randomString(),
]);
$lonely->save();
// Use the group content → group test view.
$view = Views::getView('test_og_group_content_to_group');
$view->setDisplay('default');
// Filter base rows to only our two content bundles, excluding group bundle.
$view->display_handler->setOption('filters', [
'type' => [
'id' => 'type',
'table' => 'node_field_data',
'field' => 'type',
'value' => [
$this->contentBundle => $this->contentBundle,
$lonely_bundle => $lonely_bundle,
],
'entity_type' => 'node',
'entity_field' => 'type',
'plugin_id' => 'bundle',
],
]);
// Toggle relationship to optional (LEFT join).
$relationships = $view->display_handler->getOption('relationships') ?: [];
$relationships['og_group_content_to_group__node']['required'] = FALSE;
$view->display_handler->setOption('relationships', $relationships);
// Enable DISTINCT for this test to ensure unique base rows when using
// LEFT join.
$query_option = $view->display_handler->getOption('query') ?: ['type' => 'views_query', 'options' => []];
$query_option['options']['distinct'] = TRUE;
$view->display_handler->setOption('query', $query_option);
$this->executeView($view);
// Verify both base entities are present regardless of duplicate expansion
// from the relationship.
$nids = array_map(fn($row) => (int) $row->_entity->id(), $view->result);
$unique_nids = array_values(array_unique($nids));
sort($unique_nids);
$expected_left = [
(int) $this->contentNode->id(),
(int) $lonely->id(),
];
sort($expected_left);
$this->assertEquals($expected_left, $unique_nids, 'LEFT join returns both content nodes (unique by nid).');
// Now toggle relationship to required (INNER join).
$relationships['og_group_content_to_group__node']['required'] = TRUE;
$view->display_handler->setOption('relationships', $relationships);
// Ensure the relationship join is retained by referencing a related-side
// field. Add a hidden field from the related table (group side) using the
// relationship. Rebuild the view so the updated relationship 'required'
// flag is applied.
$view->destroy();
// Use a fresh view instance to avoid any cached joins/handlers.
$view = Views::getView('test_og_group_content_to_group');
$view->setDisplay('default');
// Re-apply filters for the two content bundles.
$view->display_handler->setOption('filters', [
'type' => [
'id' => 'type',
'table' => 'node_field_data',
'field' => 'type',
'value' => [
$this->contentBundle => $this->contentBundle,
$lonely_bundle => $lonely_bundle,
],
'entity_type' => 'node',
'entity_field' => 'type',
'plugin_id' => 'bundle',
],
]);
// Ensure relationship remains required after rebuild.
$relationships = $view->display_handler->getOption('relationships') ?: [];
$relationships['og_group_content_to_group__node']['required'] = TRUE;
$view->display_handler->setOption('relationships', $relationships);
// Re-add a hidden field from the related table (group side) using the
// relationship to ensure Views keeps the relationship join active in the
// query build.
$fields = $view->display_handler->getOption('fields') ?: [];
$fields['related_group_nid'] = [
'id' => 'nid',
'table' => 'node_field_data',
'field' => 'nid',
'relationship' => 'og_group_content_to_group__node',
'plugin_id' => 'field',
'entity_type' => 'node',
'entity_field' => 'nid',
'exclude' => TRUE,
'label' => '',
];
$view->display_handler->setOption('fields', $fields);
$this->executeView($view);
// Expect only the content that has an audience reference
// (lonely node absent).
$nids_inner = array_map(fn($row) => (int) $row->_entity->id(), $view->result);
$unique_inner = array_values(array_unique($nids_inner));
sort($unique_inner);
$this->assertNotContains((int) $lonely->id(), $unique_inner, 'INNER join hides rows without relationships.');
$this->assertEquals([(int) $this->contentNode->id()], $unique_inner, 'INNER join returns only related content (unique by nid).');
}
}
