Initial Drupal 11 with DDEV setup

This commit is contained in:
gluebox
2025-10-08 11:39:17 -04:00
commit 89ef74b305
25344 changed files with 2599172 additions and 0 deletions

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for node.
*
* @group node
*/
class GenericTest extends GenericModuleTestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
// Ensure the generic test base is working as expected.
$this->assertSame('node', $this->getModule());
parent::setUp();
}
}

View File

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
/**
* Tests the persistence of basic options through multiple steps.
*
* @group node
*/
class MultiStepNodeFormBasicOptionsTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The field name to create.
*
* @var string
*/
protected $fieldName;
/**
* Tests changing the default values of basic options to ensure they persist.
*/
public function testMultiStepNodeFormBasicOptions(): void {
// Prepare a user to create the node.
$web_user = $this->drupalCreateUser([
'administer nodes',
'create page content',
]);
$this->drupalLogin($web_user);
// Create an unlimited cardinality field.
$this->fieldName = $this->randomMachineName();
FieldStorageConfig::create([
'field_name' => $this->fieldName,
'entity_type' => 'node',
'type' => 'text',
'cardinality' => -1,
])->save();
// Attach an instance of the field to the page content type.
FieldConfig::create([
'field_name' => $this->fieldName,
'entity_type' => 'node',
'bundle' => 'page',
'label' => $this->randomMachineName() . '_label',
])->save();
\Drupal::service('entity_display.repository')
->getFormDisplay('node', 'page')
->setComponent($this->fieldName, [
'type' => 'text_textfield',
])
->save();
$edit = [
'title[0][value]' => 'a',
'promote[value]' => FALSE,
'sticky[value]' => 1,
"{$this->fieldName}[0][value]" => $this->randomString(32),
];
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Add another item');
$this->assertSession()->checkboxNotChecked('edit-promote-value');
$this->assertSession()->checkboxChecked('edit-sticky-value');
}
}

View File

@ -0,0 +1,278 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\node\Traits\NodeAccessTrait;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
/**
* Tests behavior of the node access subsystem if the base table is not node.
*
* @group node
*/
class NodeAccessBaseTableTest extends NodeTestBase {
use EntityReferenceFieldCreationTrait;
use NodeAccessTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'node_access_test',
'views',
'taxonomy',
'search',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Nodes by user.
*
* @var array
*/
protected $nodesByUser;
/**
* A public tid.
*
* @var \Drupal\Core\Database\StatementInterface
*/
protected $publicTid;
/**
* A private tid.
*
* @var \Drupal\Core\Database\StatementInterface
*/
protected $privateTid;
/**
* A web user.
*
* @var \Drupal\user\Entity\User|false
*/
protected $webUser;
/**
* The nids visible.
*
* @var array
*/
protected $nidsVisible;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create the vocabulary for the tag field.
$vocabulary = Vocabulary::create([
'name' => 'Tags',
'vid' => 'tags',
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
]);
$vocabulary->save();
$field_name = 'field_' . $vocabulary->id();
$handler_settings = [
'target_bundles' => [
$vocabulary->id() => $vocabulary->id(),
],
'auto_create' => TRUE,
];
$this->createEntityReferenceField('node', 'article', $field_name, 'Tags', 'taxonomy_term', 'default', $handler_settings, FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
$entity_type_manager = $this->container->get('entity_type.manager');
$entity_type_manager
->getStorage('entity_form_display')
->load('node.article.default')
->setComponent($field_name, [
'type' => 'entity_reference_autocomplete_tags',
'weight' => -4,
])
->save();
$this->addPrivateField(NodeType::load('article'));
node_access_rebuild();
\Drupal::state()->set('node_access_test.private', TRUE);
}
/**
* Tests the "private" node access functionality.
*
* - Create 2 users with "access content" and "create article" permissions.
* - Each user creates one private and one not private article.
*
* - Test that each user can view the other user's non-private article.
* - Test that each user cannot view the other user's private article.
* - Test that each user finds only appropriate (non-private + own private)
* in taxonomy listing.
* - Create another user with 'view any private content'.
* - Test that user 4 can view all content created above.
* - Test that user 4 can view all content on taxonomy listing.
*/
public function testNodeAccessBasic(): void {
$num_simple_users = 2;
$simple_users = [];
// Nodes keyed by uid and nid: $nodes[$uid][$nid] = $is_private;
$this->nodesByUser = [];
// Titles keyed by nid.
$titles = [];
// Array of nids marked private.
$private_nodes = [];
for ($i = 0; $i < $num_simple_users; $i++) {
$simple_users[$i] = $this->drupalCreateUser([
'access content',
'create article content',
]);
}
foreach ($simple_users as $this->webUser) {
$this->drupalLogin($this->webUser);
foreach ([0 => 'Public', 1 => 'Private'] as $is_private => $type) {
$edit = [
'title[0][value]' => "$type Article created by " . $this->webUser->getAccountName(),
];
if ($is_private) {
$edit['private[0][value]'] = TRUE;
$edit['body[0][value]'] = 'private node';
$edit['field_tags[target_id]'] = 'private';
}
else {
$edit['body[0][value]'] = 'public node';
$edit['field_tags[target_id]'] = 'public';
}
$this->drupalGet('node/add/article');
$this->submitForm($edit, 'Save');
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$this->assertEquals($is_private, (int) $node->private->value, 'The private status of the node was properly set in the node_access_test table.');
if ($is_private) {
$private_nodes[] = $node->id();
}
$titles[$node->id()] = $edit['title[0][value]'];
$this->nodesByUser[$this->webUser->id()][$node->id()] = $is_private;
}
}
$public_tids = \Drupal::entityQuery('taxonomy_term')
->accessCheck(FALSE)
->condition('name', 'public')
->condition('default_langcode', 1)
->execute();
$this->publicTid = reset($public_tids);
$private_tids = \Drupal::entityQuery('taxonomy_term')
->accessCheck(FALSE)
->condition('name', 'private')
->condition('default_langcode', 1)
->execute();
$this->privateTid = reset($private_tids);
$this->assertNotEmpty($this->publicTid, 'Public tid was found');
$this->assertNotEmpty($this->privateTid, 'Private tid was found');
foreach ($simple_users as $this->webUser) {
$this->drupalLogin($this->webUser);
// Check own nodes to see that all are readable.
foreach ($this->nodesByUser as $uid => $data) {
foreach ($data as $nid => $is_private) {
$this->drupalGet('node/' . $nid);
if ($is_private) {
$should_be_visible = $uid == $this->webUser->id();
}
else {
$should_be_visible = TRUE;
}
$this->assertSession()->statusCodeEquals($should_be_visible ? 200 : 403);
}
}
// Check to see that the correct nodes are shown on taxonomy/private
// and taxonomy/public.
$this->assertTaxonomyPage(FALSE);
}
// Now test that a user with 'node test view' permissions can view content.
$access_user = $this->drupalCreateUser([
'access content',
'create article content',
'node test view',
'search content',
]);
$this->drupalLogin($access_user);
foreach ($this->nodesByUser as $private_status) {
foreach ($private_status as $nid => $is_private) {
$this->drupalGet('node/' . $nid);
$this->assertSession()->statusCodeEquals(200);
}
}
// This user should be able to see all of the nodes on the relevant
// taxonomy pages.
$this->assertTaxonomyPage(TRUE);
// Rebuild the node access permissions, repeat the test. This is done to
// ensure that node access is rebuilt correctly even if the current user
// does not have the bypass node access permission.
node_access_rebuild();
foreach ($this->nodesByUser as $private_status) {
foreach ($private_status as $nid => $is_private) {
$this->drupalGet('node/' . $nid);
$this->assertSession()->statusCodeEquals(200);
}
}
// This user should be able to see all of the nodes on the relevant
// taxonomy pages.
$this->assertTaxonomyPage(TRUE);
}
/**
* Checks taxonomy/term listings to ensure only accessible nodes are listed.
*
* @param bool $is_admin
* A boolean indicating whether the current user is an administrator. If
* TRUE, all nodes should be listed. If FALSE, only public nodes and the
* user's own private nodes should be listed.
*
* @internal
*/
protected function assertTaxonomyPage(bool $is_admin): void {
foreach ([$this->publicTid, $this->privateTid] as $tid_is_private => $tid) {
$this->drupalGet("taxonomy/term/$tid");
$this->nidsVisible = [];
foreach ($this->xpath("//a[text()='Read more']") as $link) {
// See also testTranslationRendering() in NodeTranslationUITest.
$this->assertEquals(1, preg_match('|node/(\d+)$|', $link->getAttribute('href'), $matches), 'Read more points to a node');
$this->nidsVisible[$matches[1]] = TRUE;
}
foreach ($this->nodesByUser as $uid => $data) {
foreach ($data as $nid => $is_private) {
// Private nodes should be visible on the private term page,
// public nodes should be visible on the public term page.
$should_be_visible = $tid_is_private == $is_private;
// Non-administrators can only see their own nodes on the private
// term page.
if (!$is_admin && $tid_is_private) {
$should_be_visible = $should_be_visible && $uid == $this->webUser->id();
}
$this->assertSame($should_be_visible, isset($this->nidsVisible[$nid]), strtr('A %private node by user %uid is %visible for user %current_uid on the %tid_is_private page.', ['%private' => $is_private ? 'private' : 'public', '%uid' => $uid, '%visible' => isset($this->nidsVisible[$nid]) ? 'visible' : 'not visible', '%current_uid' => $this->webUser->id(), '%tid_is_private' => $tid_is_private ? 'private' : 'public']));
}
}
}
}
}

View File

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
/**
* Tests the node access grants cache context service.
*
* @group node
* @group Cache
*/
class NodeAccessCacheRedirectWarningTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['block', 'node_access_test_empty'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
node_access_rebuild();
}
/**
* Ensures that node access checks don't cause cache redirect warnings.
*
* @covers \Drupal\node\NodeAccessControlHandler
*/
public function testNodeAccessCacheRedirectWarning(): void {
$this->drupalPlaceBlock('local_tasks_block');
// Ensure that both a node_grants implementation exists, and that the
// current user has 'view own unpublished nodes' permission. Node's access
// control handler bypasses node grants when 'view own published nodes' is
// granted and the node is unpublished, which means that the code path is
// significantly different when a node is published vs. unpublished, and
// that cache contexts vary depend on the state of the node.
$this->assertTrue(\Drupal::moduleHandler()->hasImplementations('node_grants'));
$author = $this->drupalCreateUser([
'create page content',
'edit any page content',
'view own unpublished content',
]);
$this->drupalLogin($author);
$node = $this->drupalCreateNode(['uid' => $author->id(), 'status' => 0]);
$this->drupalGet($node->toUrl());
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($node->label());
$node->setPublished();
$node->save();
$this->drupalGet($node->toUrl());
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($node->label());
// When the node has been viewed in both the unpublished and published state
// a cache redirect should exist for the local tasks block. Repeating the
// process of changing the node status and viewing the node will test that
// no stale redirect is found.
$node->setUnpublished();
$node->save();
$this->drupalGet($node->toUrl());
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($node->label());
$node->setPublished();
$node->save();
$this->drupalGet($node->toUrl());
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($node->label());
}
}

View File

@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Url;
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
/**
* Tests the node access automatic cacheability bubbling logic.
*
* @group node
* @group Cache
* @group cacheability_safeguards
*/
class NodeAccessCacheabilityTest extends NodeTestBase {
use AssertPageCacheContextsAndTagsTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'node_access_test',
'node_access_test_auto_bubbling',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
node_access_rebuild();
// Create some content.
$this->drupalCreateNode();
$this->drupalCreateNode();
$this->drupalCreateNode();
$this->drupalCreateNode();
}
/**
* Tests that the node grants cache context is auto-added, only when needed.
*
* @see node_query_node_access_alter()
*/
public function testNodeAccessCacheabilitySafeguard(): void {
// The node grants cache context should be added automatically.
$this->drupalGet(new Url('node_access_test_auto_bubbling'));
$this->assertCacheContext('user.node_grants:view');
// The user has the 'bypass node access' permission, which means the
// node grants cache context is not necessary.
$this->drupalLogin($this->drupalCreateUser(['bypass node access']));
$this->drupalGet(new Url('node_access_test_auto_bubbling'));
$this->assertNoCacheContext('user.node_grants:view');
$this->drupalLogout();
// Uninstall the module with the only hook_node_grants() implementation.
$this->container->get('module_installer')->uninstall(['node_access_test']);
$this->rebuildContainer();
// Because there are no node grants defined, there also is no need for the
// node grants cache context to be bubbled.
$this->drupalGet(new Url('node_access_test_auto_bubbling'));
$this->assertNoCacheContext('user.node_grants:view');
}
/**
* Tests that the user cache contexts are correctly set.
*/
public function testNodeAccessCacheContext(): void {
// Create a user, with edit/delete own content permission.
$test_user1 = $this->drupalCreateUser([
'access content',
'edit own page content',
'delete own page content',
]);
$this->drupalLogin($test_user1);
$node1 = $this->createNode(['type' => 'page']);
// User should be able to edit/delete their own content.
// Therefore after the access check in node_node_access the user cache
// context should be added.
$this->drupalGet('node/' . $node1->id() . '/edit');
$this->assertCacheContext('user');
$this->drupalGet('node/' . $node1->id() . '/delete');
$this->assertCacheContext('user');
// Create a user without edit/delete permission.
$test_user2 = $this->drupalCreateUser([
'access content',
]);
$this->drupalLogin($test_user2);
$node2 = $this->createNode(['type' => 'page']);
// The user shouldn't have access to the node edit/delete pages.
// Therefore after the access check in node_node_access the user permissions
// cache context should be added.
$this->drupalGet('node/' . $node2->id() . '/edit');
$this->assertCacheContext('user.permissions');
$this->drupalGet('node/' . $node2->id() . '/delete');
$this->assertCacheContext('user.permissions');
}
}

View File

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
/**
* Tests node view access cacheability with node grants.
*
* @group node
*/
class NodeAccessCacheabilityWithNodeGrantsTest extends BrowserTestBase {
use EntityReferenceFieldCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'node_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests node view access cacheability with node grants.
*/
public function testAccessCacheabilityWithNodeGrants(): void {
NodeType::create(['type' => 'page', 'name' => 'Page'])->save();
$this->createEntityReferenceField('node', 'page', 'ref', 'Ref', 'node');
EntityViewDisplay::create([
'targetEntityType' => 'node',
'bundle' => 'page',
'mode' => 'default',
'status' => TRUE,
])->setComponent('ref', ['type' => 'entity_reference_label'])
->save();
// Check that at least one module implements hook_node_grants() as this test
// only tests this case.
// @see \node_test_node_grants()
$this->assertTrue(\Drupal::moduleHandler()->hasImplementations('node_grants'));
// Create an unpublished node.
$referenced = $this->createNode(['status' => FALSE]);
// Create a node referencing $referenced.
$node = $this->createNode(['ref' => $referenced]);
// Check that the referenced entity link doesn't show on the host entity.
$this->drupalGet($node->toUrl());
$this->assertSession()->linkNotExists($referenced->label());
// Publish the referenced node.
$referenced->setPublished()->save();
// Check that the referenced entity link shows on the host entity.
$this->getSession()->reload();
$this->assertSession()->linkExists($referenced->label());
}
}

View File

@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
/**
* Tests the interaction of the node access system with fields.
*
* @group node
*/
class NodeAccessFieldTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node_access_test', 'field_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A user with permission to bypass access content.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* A user with permission to manage content types and fields.
*
* @var \Drupal\user\UserInterface
*/
protected $contentAdminUser;
/**
* The name of the created field.
*
* @var string
*/
protected $fieldName;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
node_access_rebuild();
// Create some users.
$this->adminUser = $this->drupalCreateUser([
'access content',
'bypass node access',
]);
$this->contentAdminUser = $this->drupalCreateUser([
'access content',
'administer content types',
'administer node fields',
]);
// Add a custom field to the page content type.
$this->fieldName = $this->randomMachineName() . '_field_name';
FieldStorageConfig::create([
'field_name' => $this->fieldName,
'entity_type' => 'node',
'type' => 'text',
])->save();
FieldConfig::create([
'field_name' => $this->fieldName,
'entity_type' => 'node',
'bundle' => 'page',
])->save();
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
$display_repository->getViewDisplay('node', 'page')
->setComponent($this->fieldName)
->save();
$display_repository->getFormDisplay('node', 'page')
->setComponent($this->fieldName)
->save();
}
/**
* Tests administering fields when node access is restricted.
*/
public function testNodeAccessAdministerField(): void {
// Create a page node.
$fieldData = [];
$value = $fieldData[0]['value'] = $this->randomMachineName();
$node = $this->drupalCreateNode([$this->fieldName => $fieldData]);
// Log in as the administrator and confirm that the field value is present.
$this->drupalLogin($this->adminUser);
$this->drupalGet('node/' . $node->id());
$this->assertSession()->pageTextContains($value);
// Log in as the content admin and try to view the node.
$this->drupalLogin($this->contentAdminUser);
$this->drupalGet('node/' . $node->id());
$this->assertSession()->pageTextContains('Access denied');
// Modify the field default as the content admin.
$edit = [
'set_default_value' => '1',
];
$default = 'Sometimes words have two meanings';
$edit["default_value_input[{$this->fieldName}][0][value]"] = $default;
$this->drupalGet("admin/structure/types/manage/page/fields/node.page.{$this->fieldName}");
$this->submitForm($edit, 'Save settings');
// Log in as the administrator.
$this->drupalLogin($this->adminUser);
// Confirm that the existing node still has the correct field value.
$this->drupalGet('node/' . $node->id());
$this->assertSession()->pageTextContains($value);
// Confirm that the new default value appears when creating a new node.
$this->drupalGet('node/add/page');
$this->assertSession()->responseContains($default);
}
}

View File

@ -0,0 +1,185 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Database\Database;
use Drupal\user\Entity\User;
/**
* Tests the node access grants cache context service.
*
* @group node
* @group Cache
*/
class NodeAccessGrantsCacheContextTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node_access_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* User with permission to view content.
*
* @var \Drupal\user\Entity\User|false
*/
protected $accessUser;
/**
* User without permission to view content.
*
* @var \Drupal\user\Entity\User|false
*/
protected $noAccessUser;
/**
* User without permission to view content.
*
* @var \Drupal\user\Entity\User
*/
protected User $noAccessUser2;
/**
* User with permission to bypass node access.
*
* @var \Drupal\user\Entity\User|false
*
* @see \Drupal\Tests\user\Traits\UserCreationTrait::createUser
*/
protected $adminUser;
/**
* @var array
*/
protected array $userMapping;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
node_access_rebuild();
// Create some content.
$this->drupalCreateNode();
$this->drupalCreateNode();
$this->drupalCreateNode();
$this->drupalCreateNode();
// Create user with simple node access permission. The 'node test view'
// permission is implemented and granted by the node_access_test module.
$this->accessUser = $this->drupalCreateUser([
'access content overview',
'access content',
'node test view',
]);
$this->noAccessUser = $this->drupalCreateUser([
'access content overview',
'access content',
]);
$this->noAccessUser2 = $this->drupalCreateUser([
'access content overview',
'access content',
]);
$this->adminUser = $this->drupalCreateUser([
'bypass node access',
]);
$this->userMapping = [
1 => $this->adminUser,
2 => $this->accessUser,
3 => $this->noAccessUser,
];
}
/**
* Asserts that for each given user, the expected cache context is returned.
*
* @param array $expected
* Expected values, keyed by user ID, expected cache contexts as values.
*
* @internal
*/
protected function assertUserCacheContext(array $expected): void {
foreach ($expected as $uid => $context) {
if ($uid > 0) {
$this->drupalLogin($this->userMapping[$uid]);
}
$this->assertSame($context, $this->container->get('cache_context.user.node_grants')->getContext('view'));
}
$this->drupalLogout();
}
/**
* Tests NodeAccessGrantsCacheContext::getContext().
*/
public function testCacheContext(): void {
$this->assertUserCacheContext([
0 => 'view.all:0;node_access_test_author:0;node_access_all:0',
1 => 'all',
2 => 'view.all:0;node_access_test_author:2;node_access_test:8888,8889',
3 => 'view.all:0;node_access_test_author:3',
]);
// Grant view to all nodes (because nid = 0) for users in the
// 'node_access_all' realm.
$record = [
'nid' => 0,
'gid' => 0,
'realm' => 'node_access_all',
'grant_view' => 1,
'grant_update' => 0,
'grant_delete' => 0,
];
Database::getConnection()->insert('node_access')->fields($record)->execute();
// Put user accessUser (uid 0) in the realm.
\Drupal::state()->set('node_access_test.no_access_uid', 0);
drupal_static_reset('node_access_view_all_nodes');
$this->assertUserCacheContext([
0 => 'view.all',
1 => 'all',
2 => 'view.all:0;node_access_test_author:2;node_access_test:8888,8889',
3 => 'view.all:0;node_access_test_author:3',
]);
// Put user accessUser (uid 2) in the realm.
\Drupal::state()->set('node_access_test.no_access_uid', $this->accessUser->id());
drupal_static_reset('node_access_view_all_nodes');
$this->assertUserCacheContext([
0 => 'view.all:0;node_access_test_author:0',
1 => 'all',
2 => 'view.all',
3 => 'view.all:0;node_access_test_author:3',
]);
// Put user noAccessUser (uid 3) in the realm.
\Drupal::state()->set('node_access_test.no_access_uid', $this->noAccessUser->id());
drupal_static_reset('node_access_view_all_nodes');
$this->assertUserCacheContext([
0 => 'view.all:0;node_access_test_author:0',
1 => 'all',
2 => 'view.all:0;node_access_test_author:2;node_access_test:8888,8889',
3 => 'view.all',
]);
// Uninstall the node_access_test module
$this->container->get('module_installer')->uninstall(['node_access_test']);
drupal_static_reset('node_access_view_all_nodes');
$this->assertUserCacheContext([
0 => 'view.all',
1 => 'all',
2 => 'view.all',
3 => 'view.all',
]);
}
}

View File

@ -0,0 +1,359 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\node\Traits\NodeAccessTrait;
use Drupal\user\UserInterface;
use Drupal\views\Tests\ViewTestData;
/**
* Tests Node Access on join.
*
* @group views
*/
class NodeAccessJoinTest extends NodeTestBase {
use NodeAccessTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['node_access_test', 'node_test_views', 'views'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The user that will create the articles.
*
* @var \Drupal\user\UserInterface
*/
protected UserInterface $authorUser;
/**
* Another user that will create articles.
*
* @var \Drupal\user\UserInterface
*/
protected UserInterface $otherUser;
/**
* A user with just access content permissions.
*
* @var \Drupal\user\UserInterface
*/
protected UserInterface $regularUser;
/**
* A user with access to private articles.
*
* @var \Drupal\user\UserInterface
*/
protected UserInterface $accessUser;
/**
* Articles.
*
* @var array
*/
protected array $articles;
/**
* Views used by this test.
*
* @var array
*/
public static array $testViews = ['test_node_access_join'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->addPrivateField(NodeType::load('article'));
$field_storage = FieldStorageConfig::create([
'field_name' => 'related_article',
'entity_type' => 'node',
'translatable' => FALSE,
'entity_types' => [],
'settings' => [
'target_type' => 'node',
],
'type' => 'entity_reference',
]);
$field_storage->save();
$field = FieldConfig::create([
'field_name' => 'related_article',
'entity_type' => 'node',
'bundle' => 'page',
'label' => 'Related Article',
'settings' => [
'handler' => 'default',
'handler_settings' => [
// Reference a single vocabulary.
'target_bundles' => [
'article',
],
],
],
]);
$field->save();
$entity_display = \Drupal::service('entity_display.repository');
$entity_display->getViewDisplay('node', 'page', 'default')
->setComponent('related_article')
->save();
$entity_display->getFormDisplay('node', 'page', 'default')
->setComponent('related_article', [
'type' => 'entity_reference_autocomplete',
])
->save();
$field = FieldConfig::create([
'field_name' => 'related_article',
'entity_type' => 'node',
'bundle' => 'article',
'label' => 'Related Article',
'settings' => [
'handler' => 'default',
'handler_settings' => [
// Reference a single vocabulary.
'target_bundles' => [
'article',
],
],
],
]);
$field->save();
$entity_display->getViewDisplay('node', 'article', 'default')
->setComponent('related_article')
->save();
$entity_display->getFormDisplay('node', 'article', 'default')
->setComponent('related_article', [
'type' => 'entity_reference_autocomplete',
])
->save();
node_access_rebuild();
\Drupal::state()->set('node_access_test.private', TRUE);
}
/**
* Tests the accessibility of joined nodes.
*
* - Create two users with "access content" and "create article" permissions
* who can each access their own private articles but not others'.
* - Create article-type nodes with and without references to other articles.
* The articles and references represent all possible combinations of the
* tested access types.
* - Create page-type nodes referencing each of the articles, as well as a
* page with no reference.
* - Use a custom view that creates two joins between nodes and has a
* node_access tag. The view lists the page nodes, the article
* referenced by each page node, and the article referenced by each
* article.
*
* - Login with the author user and check that user does not have access to
* private nodes created by other users. Test access using total row
* count as well as checking for presence of individual page titles.
* - Repeat tests using a user with only the "access content" permission,
* confirming this user does not have access to any private nodes.
* - Repeat tests using a user with "access content" and "node test view"
* permissions, confirming this user sees the complete view.
*/
public function testNodeAccessJoin(): void {
$permissions = ['access content', 'create article content'];
// User to add articles and test author access.
$this->authorUser = $this->drupalCreateUser($permissions);
// Another user to add articles whose private articles can not be accessed
// by authorUser.
$this->otherUser = $this->drupalCreateUser($permissions);
// Create the articles. The articles are stored in an array keyed by
// $article and $reference2, where $article is the access type of the
// article itself, and $reference2 is the access type of the reference
// linked to by the article. 'public' articles are created by otherUser with
// private=0. 'private' articles are created by otherUser with private=1.
// 'author_public' articles are created by authorUser with private=0.
// 'author_private' articles are created by authorUser with private=1.
// 'no_reference' is used for references when there is no related article.
$access_type = ['public', 'private', 'author_public', 'author_private'];
$reference_access_type = array_merge(['no_reference'], $access_type);
foreach ($reference_access_type as $reference2) {
foreach ($access_type as $article) {
$is_author = (str_starts_with($article, 'author'));
$is_private = (str_ends_with($article, 'private'));
$edit = [
'type' => 'article',
'uid' => $is_author ? $this->authorUser->id() : $this->otherUser->id(),
];
$edit['private'][0]['value'] = $is_private;
// The article names provide the access status of the article and the
// access status of the related article, if any. The naming system
// ensures that the text 'Article $article' will only appear in the view
// if an article with that access type is displayed in the view. The
// text '$article' alone will appear in the titles of other nodes that
// reference an article.
$edit['title'] = "Article $article - $reference2";
if ($reference2 !== 'no_reference') {
$edit['related_article'][0]['target_id'] = $this->articles[$reference2]['no_reference'];
}
$node = $this->drupalCreateNode($edit);
$this->articles[$article][$reference2] = $node->id();
$this->assertEquals((int) $is_private, (int) $node->private->value, 'The private status of the article node was properly set in the node_access_test table.' . $node->uid->target_id);
if ($reference2 !== 'no_reference') {
$this->assertEquals((int) $this->articles[$reference2]['no_reference'], (int) $node->related_article->target_id, 'Proper article attached to article.');
}
}
}
// Add a blank 'no_reference' entry to the article list, so that a page with
// no reference gets created.
$this->articles['no_reference']['no_reference'] = NULL;
$total = 0;
$count_s_total = $count_s2_total = 0;
$count_s_public = $count_s2_public = 0;
$count_s_author = $count_s2_author = 0;
$total_public = $total_author = 0;
// Create page nodes referencing each article, as a page without reference.
foreach ($this->articles as $reference => $list) {
foreach ($list as $reference2 => $article_nid) {
$title = "Page - $reference";
if ($reference !== 'no_reference') {
$title .= " - $reference2";
}
$edit = [
'type' => 'page',
'title' => $title,
];
if ($article_nid) {
$edit['related_article'][0]['target_id'] = $article_nid;
}
$node = $this->drupalCreateNode($edit);
if ($article_nid) {
$this->assertEquals((int) $article_nid, (int) $node->related_article->target_id, 'Proper article attached to page.');
}
// Calculate totals expected for each user type.
$total++;
// Total number of primary and secondary references.
if ($reference !== 'no_reference') {
$count_s_total++;
if ($reference2 !== 'no_reference') {
$count_s2_total++;
}
}
// Public users only see 'public' and 'author_public' articles.
if (str_ends_with($reference, 'public')) {
$count_s_public++;
if (str_ends_with($reference2, 'public')) {
$count_s2_public++;
}
}
// authorUser sees 'public','author_public', 'author_private' articles.
if (str_ends_with($reference, 'public') || str_starts_with($reference, 'author')) {
$count_s_author++;
if (str_ends_with($reference2, 'public') || str_starts_with($reference2, 'author')) {
$count_s2_author++;
}
}
// $total_public and $total_author are not currently in use -- but
// represent the totals when joins are handled by adding an is-null
// check (i.e., if inaccessible references caused the entire row to be
// hidden from view, instead of hiding just one cell of the table).
// Count of pages where all related articles are accessible by
// public users.
if (!str_ends_with($reference, 'private') && !str_ends_with($reference2, 'private')) {
$total_public++;
}
// Count of pages where all related articles are accessible by
// authorUser.
if ($reference !== 'private' && $reference2 !== 'private') {
$total_author++;
}
}
}
// Generate a view listing all the pages, and check the view's content for
// users with three different access levels.
ViewTestData::createTestViews(get_class($this), ['node_test_views']);
// Check the author of the 'author' articles.
$this->drupalLogin($this->authorUser);
$this->drupalGet('test-node-access-join');
$chk_total = count($this->xpath("//td[@headers='view-title-table-column']"));
$this->assertEquals($chk_total, $total, 'Author should see ' . $total . ' rows. Actual: ' . $chk_total);
$chk_total = count($this->xpath("//td[@headers='view-title-1-table-column']/a"));
$this->assertEquals($chk_total, $count_s_author, 'Author should see ' . $count_s_author . ' primary references. Actual: ' . $chk_total);
$chk_total = count($this->xpath("//td[@headers='view-title-2-table-column']/a"));
$this->assertEquals($chk_total, $count_s2_author, 'Author should see ' . $count_s2_author . ' secondary references. Actual: ' . $chk_total);
$session = $this->assertSession();
$session->pageTextContains('Page - no_reference');
$session->pageTextContains('Page - public - no_reference');
$session->pageTextContains('Page - public - public');
$session->pageTextContains('Page - author_private - no_reference');
$session->pageTextContains('Article public');
$session->pageTextNotContains('Article private');
$session->pageTextContains('Article author_public');
$session->pageTextContains('Article author_private');
// Check a regular user who did not author any articles.
$this->regularUser = $this->drupalCreateUser(['access content']);
$this->drupalLogin($this->regularUser);
$this->drupalGet('test-node-access-join');
$chk_total = count($this->xpath("//td[@headers='view-title-table-column']"));
$this->assertEquals($chk_total, $total, 'Public user should see ' . $total . ' rows. Actual: ' . $chk_total);
$chk_total = count($this->xpath("//td[@headers='view-title-1-table-column']/a"));
$this->assertEquals($chk_total, $count_s_public, 'Public user should see ' . $count_s_public . ' primary references. Actual: ' . $chk_total);
$chk_total = count($this->xpath("//td[@headers='view-title-2-table-column']/a"));
$this->assertEquals($chk_total, $count_s2_public, 'Public user should see ' . $count_s2_public . ' secondary references. Actual: ' . $chk_total);
$session->pageTextContains('Page - no_reference');
$session->pageTextContains('Page - public - no_reference');
$session->pageTextContains('Page - public - public');
$session->pageTextContains('Article public');
$session->pageTextNotContains('Article private');
$session->pageTextContains('Article author_public');
$session->pageTextNotContains('Article author_private');
// Check that a user with 'node test view' permission, can view all pages
// and articles.
$this->accessUser = $this->drupalCreateUser([
'access content',
'node test view',
]);
$this->drupalLogin($this->accessUser);
$this->drupalGet('test-node-access-join');
$chk_total = count($this->xpath("//td[@headers='view-title-table-column']"));
$this->assertEquals($chk_total, $total, 'Full-access user should see ' . $total . ' rows. Actual: ' . $chk_total);
$chk_total = count($this->xpath("//td[@headers='view-title-1-table-column']/a"));
$this->assertEquals($chk_total, $count_s_total, 'Full-access user should see ' . $count_s_total . ' primary references. Actual: ' . $chk_total);
$chk_total = count($this->xpath("//td[@headers='view-title-2-table-column']/a"));
$this->assertEquals($chk_total, $count_s2_total, 'Full-access user should see ' . $count_s2_total . ' secondary references. Actual: ' . $chk_total);
$session->pageTextContains('Page - no_reference');
$session->pageTextContains('Page - public - no_reference');
$session->pageTextContains('Page - public - public');
$session->pageTextContains('Page - author_private - no_reference');
$session->pageTextContains('Article public');
$session->pageTextContains('Article private');
$session->pageTextContains('Article author_public');
$session->pageTextContains('Article author_private');
}
}

View File

@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\language\Entity\ConfigurableLanguage;
/**
* Tests that the node_access system stores the proper fallback marker.
*
* @group node
*/
class NodeAccessLanguageFallbackTest extends NodeTestBase {
/**
* Enable language and a non-language-aware node access module.
*
* @var array
*/
protected static $modules = [
'language',
'node_access_test',
'content_translation',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// After enabling a node access module, the {node_access} table has to be
// rebuilt.
node_access_rebuild();
// Add Hungarian, Catalan, and Afrikaans.
ConfigurableLanguage::createFromLangcode('hu')->save();
ConfigurableLanguage::createFromLangcode('ca')->save();
ConfigurableLanguage::createFromLangcode('af')->save();
// Enable content translation for the current entity type.
\Drupal::service('content_translation.manager')->setEnabled('node', 'page', TRUE);
}
/**
* Tests node access fallback handling with multiple node languages.
*/
public function testNodeAccessLanguageFallback(): void {
// The node_access_test module allows nodes to be marked private. We need to
// ensure that system honors the fallback system of node access properly.
// Note that node_access_test_language is language-sensitive and does not
// apply to the fallback test.
// Create one node in Hungarian and marked as private.
$node = $this->drupalCreateNode([
'body' => [[]],
'langcode' => 'hu',
'private' => [['value' => 1]],
'status' => 1,
]);
// There should be one entry in node_access, with fallback set to hu.
$this->checkRecords(1, 'hu');
// Create a translation user.
$admin = $this->drupalCreateUser([
'bypass node access',
'administer nodes',
'translate any entity',
'administer content translation',
]);
$this->drupalLogin($admin);
$this->drupalGet('node/' . $node->id() . '/translations');
$this->assertSession()->statusCodeEquals(200);
// Create a Catalan translation through the UI.
$url_options = ['language' => \Drupal::languageManager()->getLanguage('ca')];
$this->drupalGet('node/' . $node->id() . '/translations/add/hu/ca', $url_options);
$this->assertSession()->statusCodeEquals(200);
// Save the form.
$this->getSession()->getPage()->pressButton('Save (this translation)');
$this->assertSession()->statusCodeEquals(200);
// Check the node access table.
$this->checkRecords(2, 'hu');
// Programmatically create a translation. This process lets us check that
// both forms and code behave in the same way.
$storage = \Drupal::entityTypeManager()->getStorage('node');
// Reload the node.
$node = $storage->load(1);
// Create an Afrikaans translation.
$translation = $node->addTranslation('af');
$translation->title->value = $this->randomString();
$translation->status = 1;
$node->save();
// Check the node access table.
$this->checkRecords(3, 'hu');
// For completeness, edit the Catalan version again.
$this->drupalGet('node/' . $node->id() . '/edit', $url_options);
$this->assertSession()->statusCodeEquals(200);
// Save the form.
$this->getSession()->getPage()->pressButton('Save (this translation)');
$this->assertSession()->statusCodeEquals(200);
// Check the node access table.
$this->checkRecords(3, 'hu');
}
/**
* Queries the node_access table and checks for proper storage.
*
* @param int $count
* The number of rows expected by the query (equal to the translation
* count).
* @param string $langcode
* The expected language code set as the fallback property.
*/
public function checkRecords($count, $langcode = 'hu'): void {
$select = \Drupal::database()
->select('node_access', 'na')
->fields('na', ['nid', 'fallback', 'langcode', 'grant_view'])
->condition('na.realm', 'node_access_test', '=')
->condition('na.gid', 8888, '=');
$records = $select->execute()->fetchAll();
// Check that the expected record count is returned.
$this->assertCount($count, $records);
// The fallback value is 'hu' and should be set to 1. For other languages,
// it should be set to 0. Casting to boolean lets us run that comparison.
foreach ($records as $record) {
$this->assertEquals((bool) $record->fallback, $record->langcode === $langcode);
}
}
}

View File

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\user\RoleInterface;
/**
* Tests the interaction of the node access system with menu links.
*
* @group node
*/
class NodeAccessMenuLinkTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['menu_ui', 'block'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A user with permission to manage menu links and create nodes.
*
* @var \Drupal\user\UserInterface
*/
protected $contentAdminUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalPlaceBlock('system_menu_block:main');
$this->contentAdminUser = $this->drupalCreateUser([
'access content',
'administer content types',
'bypass node access',
'administer menu',
]);
$this->config('user.role.' . RoleInterface::ANONYMOUS_ID)->set('permissions', [])->save();
}
/**
* SA-CORE-2015-003: Tests menu links to nodes when node access is restricted.
*/
public function testNodeAccessMenuLink(): void {
$menu_link_title = 'Test menu link title';
$this->drupalLogin($this->contentAdminUser);
$edit = [
'title[0][value]' => $this->randomString(),
'body[0][value]' => $this->randomString(),
'menu[enabled]' => 1,
'menu[title]' => $menu_link_title,
];
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
$this->assertSession()->linkExists($menu_link_title);
// Ensure anonymous users without "access content" permission do not see
// this menu link.
$this->drupalLogout();
$this->drupalGet('');
$this->assertSession()->linkNotExists($menu_link_title);
// Ensure anonymous users with "access content" permission see this menu
// link.
$this->config('user.role.' . RoleInterface::ANONYMOUS_ID)->set('permissions', ['access content'])->save();
$this->drupalGet('');
$this->assertSession()->linkExists($menu_link_title);
}
}

View File

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\comment\CommentInterface;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\comment\Entity\Comment;
use Drupal\Tests\BrowserTestBase;
use Drupal\user\Entity\User;
/**
* Tests access controlled node views have the right amount of comment pages.
*
* @group node
*/
class NodeAccessPagerTest extends BrowserTestBase {
use CommentTestTrait;
/**
* An user.
*
* @var \Drupal\user\Entity\User
*/
protected User $webUser;
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'node_access_test', 'comment'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
node_access_rebuild();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
$this->addDefaultCommentField('node', 'page');
$this->webUser = $this->drupalCreateUser([
'access content',
'access comments',
'node test view',
]);
}
/**
* Tests the comment pager for nodes with multiple grants per realm.
*/
public function testCommentPager(): void {
// Create a node.
$node = $this->drupalCreateNode();
// Create 60 comments.
for ($i = 0; $i < 60; $i++) {
$comment = Comment::create([
'entity_id' => $node->id(),
'entity_type' => 'node',
'field_name' => 'comment',
'subject' => $this->randomMachineName(),
'comment_body' => [
['value' => $this->randomMachineName()],
],
'status' => CommentInterface::PUBLISHED,
]);
$comment->save();
}
$this->drupalLogin($this->webUser);
// View the node page. With the default 50 comments per page there should
// be two pages (0, 1) but no third (2) page.
$this->drupalGet('node/' . $node->id());
$this->assertSession()->pageTextContains($node->label());
$this->assertSession()->pageTextContains('Comments');
$this->assertSession()->responseContains('page=1');
$this->assertSession()->responseNotContains('page=2');
}
}

View File

@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\node\Traits\NodeAccessTrait;
/**
* Tests node access rebuild functions with multiple node access modules.
*
* @group node
*/
class NodeAccessRebuildNodeGrantsTest extends NodeTestBase {
use NodeAccessTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A user to create nodes that only it has access to.
*
* @var \Drupal\user\UserInterface
*/
protected $webUser;
/**
* A user to test the rebuild nodes feature which can't access the nodes.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->adminUser = $this->drupalCreateUser([
'administer site configuration',
'access administration pages',
'access site reports',
'administer nodes',
]);
$this->drupalLogin($this->adminUser);
$this->webUser = $this->drupalCreateUser();
}
/**
* Tests rebuilding the node access permissions table with content.
*/
public function testNodeAccessRebuildNodeGrants(): void {
\Drupal::service('module_installer')->install(['node_access_test']);
\Drupal::state()->set('node_access_test.private', TRUE);
$this->addPrivateField(NodeType::load('page'));
$this->resetAll();
// Create 30 nodes so that _node_access_rebuild_batch_operation() has to run
// more than once.
for ($i = 0; $i < 30; $i++) {
$nodes[] = $this->drupalCreateNode([
'uid' => $this->webUser->id(),
'private' => [['value' => 1]],
]);
}
/** @var \Drupal\node\NodeGrantDatabaseStorageInterface $grant_storage */
$grant_storage = \Drupal::service('node.grant_storage');
// Default realm access and node records are present.
foreach ($nodes as $node) {
$this->assertNotEmpty($node->private->value);
$this->assertTrue($grant_storage->access($node, 'view', $this->webUser)->isAllowed(), 'Prior to rebuilding node access the grant storage returns allowed for the node author.');
$this->assertTrue($grant_storage->access($node, 'view', $this->adminUser)->isAllowed(), 'Prior to rebuilding node access the grant storage returns allowed for the admin user.');
}
$this->assertEquals(1, \Drupal::service('node.grant_storage')->checkAll($this->webUser), 'There is an all realm access record');
$this->assertTrue(\Drupal::state()->get('node.node_access_needs_rebuild'), 'Node access permissions need to be rebuilt');
// Rebuild permissions.
$this->drupalGet('admin/reports/status');
$this->clickLink('Rebuild permissions');
$this->submitForm([], 'Rebuild permissions');
$this->assertSession()->pageTextContains('The content access permissions have been rebuilt.');
// Test if the rebuild by user that cannot bypass node access and does not
// have access to the nodes has been successful.
$this->assertFalse($this->adminUser->hasPermission('bypass node access'));
$this->assertNull(\Drupal::state()->get('node.node_access_needs_rebuild'), 'Node access permissions have been rebuilt');
foreach ($nodes as $node) {
$this->assertTrue($grant_storage->access($node, 'view', $this->webUser)->isAllowed(), 'After rebuilding node access the grant storage returns allowed for the node author.');
$this->assertFalse($grant_storage->access($node, 'view', $this->adminUser)->isForbidden(), 'After rebuilding node access the grant storage returns forbidden for the admin user.');
}
$this->assertEmpty(\Drupal::service('node.grant_storage')->checkAll($this->webUser), 'There is no all realm access record');
// Test an anonymous node access rebuild from code.
$this->drupalLogout();
node_access_rebuild();
foreach ($nodes as $node) {
$this->assertTrue($grant_storage->access($node, 'view', $this->webUser)->isAllowed(), 'After rebuilding node access the grant storage returns allowed for the node author.');
$this->assertFalse($grant_storage->access($node, 'view', $this->adminUser)->isForbidden(), 'After rebuilding node access the grant storage returns forbidden for the admin user.');
}
$this->assertEmpty(\Drupal::service('node.grant_storage')->checkAll($this->webUser), 'There is no all realm access record');
}
/**
* Tests rebuilding the node access permissions table with no content.
*/
public function testNodeAccessRebuildNoAccessModules(): void {
// Default realm access is present.
$this->assertEquals(1, \Drupal::service('node.grant_storage')->count(), 'There is an all realm access record');
// No need to rebuild permissions.
$this->assertNull(\Drupal::state()->get('node.node_access_needs_rebuild'), 'Node access permissions need to be rebuilt');
// Rebuild permissions.
$this->drupalGet('admin/reports/status');
$this->clickLink('Rebuild permissions');
$this->submitForm([], 'Rebuild permissions');
$this->assertSession()->pageTextContains('Content permissions have been rebuilt.');
$this->assertNull(\Drupal::state()->get('node.node_access_needs_rebuild'), 'Node access permissions have been rebuilt');
// Default realm access is still present.
$this->assertEquals(1, \Drupal::service('node.grant_storage')->count(), 'There is an all realm access record');
}
}

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\node\NodeInterface;
/**
* Tests cacheability on unpublished nodes inherited from node access.
*
* @group node
* @group Cache
*/
class NodeAccessUnpublishedCacheabilityTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'node_access_test_auto_bubbling',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests correct cacheability information bubbles up from node access.
*/
public function testNodeAccessCacheabilityBubbleUpOnUnpublishedContent(): void {
$rid = $this->drupalCreateRole([
'access content',
'view own unpublished content',
]);
$test_user1 = $this->drupalCreateUser(values: ['roles' => [$rid]]);
$test_user2 = $this->drupalCreateUser(values: ['roles' => [$rid]]);
$unpublished_node_by_test_user1 = $this->createNode(['type' => 'page', 'uid' => $test_user1->id(), 'status' => NodeInterface::NOT_PUBLISHED]);
$this->drupalLogin($test_user2);
$this->drupalGet('node_access_test_auto_bubbling_node_access/' . $unpublished_node_by_test_user1->id());
$this->assertSession()->pageTextNotContains($unpublished_node_by_test_user1->label());
// The author of the unpublished node must have access.
$this->drupalLogin($test_user1);
$this->drupalGet('node_access_test_auto_bubbling_node_access/' . $unpublished_node_by_test_user1->id());
$this->assertSession()->pageTextContains($unpublished_node_by_test_user1->label());
}
}

View File

@ -0,0 +1,350 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Database\Database;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\node\Entity\NodeType;
use Drupal\node\NodeInterface;
use Drupal\Tests\node\Traits\NodeAccessTrait;
use Drupal\user\RoleInterface;
/**
* Tests node administration page functionality.
*
* @group node
*/
class NodeAdminTest extends NodeTestBase {
use NodeAccessTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A user with permission to bypass access content.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* A user with the 'access content overview' permission.
*
* @var \Drupal\user\UserInterface
*/
protected $baseUser1;
/**
* A normal user with permission to view own unpublished content.
*
* @var \Drupal\user\UserInterface
*/
protected $baseUser2;
/**
* A normal user with permission to bypass node access content.
*
* @var \Drupal\user\UserInterface
*/
protected $baseUser3;
/**
* {@inheritdoc}
*/
protected static $modules = ['views'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Remove the "view own unpublished content" permission which is set
// by default for authenticated users so we can test this permission
// correctly.
user_role_revoke_permissions(RoleInterface::AUTHENTICATED_ID, ['view own unpublished content']);
$this->adminUser = $this->drupalCreateUser([
'access administration pages',
'access content overview',
'administer nodes',
'bypass node access',
]);
$this->baseUser1 = $this->drupalCreateUser(['access content overview']);
$this->baseUser2 = $this->drupalCreateUser([
'access content overview',
'view own unpublished content',
]);
$this->baseUser3 = $this->drupalCreateUser([
'access content overview',
'bypass node access',
]);
}
/**
* Tests that the table sorting works on the content admin pages.
*/
public function testContentAdminSort(): void {
$this->drupalLogin($this->adminUser);
$changed = \Drupal::time()->getRequestTime();
$connection = Database::getConnection();
foreach (['dd', 'aa', 'DD', 'bb', 'cc', 'CC', 'AA', 'BB'] as $prefix) {
$changed += 1000;
$node = $this->drupalCreateNode(['title' => $prefix . $this->randomMachineName(6)]);
$connection->update('node_field_data')
->fields(['changed' => $changed])
->condition('nid', $node->id())
->execute();
}
// Test that the default sort by node.changed DESC actually fires properly.
$nodes_query = $connection->select('node_field_data', 'n')
->fields('n', ['title'])
->orderBy('changed', 'DESC')
->execute()
->fetchCol();
$this->drupalGet('admin/content');
foreach ($nodes_query as $delta => $string) {
// Verify that the node was found in the correct order.
$this->assertSession()->elementExists('xpath', $this->assertSession()->buildXPathQuery('//table/tbody/tr[' . ($delta + 1) . ']/td[2]/a[normalize-space(text())=:label]', [
':label' => $string,
]));
}
// Compare the rendered HTML node list to a query for the nodes ordered by
// title to account for possible database-dependent sort order.
$nodes_query = $connection->select('node_field_data', 'n')
->fields('n', ['title'])
->orderBy('title')
->execute()
->fetchCol();
$this->drupalGet('admin/content', ['query' => ['sort' => 'asc', 'order' => 'title']]);
foreach ($nodes_query as $delta => $string) {
// Verify that the node was found in the correct order.
$this->assertSession()->elementExists('xpath', $this->assertSession()->buildXPathQuery('//table/tbody/tr[' . ($delta + 1) . ']/td[2]/a[normalize-space(text())=:label]', [
':label' => $string,
]));
}
// Verify aria-sort is present and its value matches the sort order.
$this->assertSession()->elementAttributeContains('css', 'table thead tr th.views-field-title', 'aria-sort', 'ascending');
}
/**
* Tests content overview with different user permissions.
*
* Taxonomy filters are tested separately.
*
* @see TaxonomyNodeFilterTestCase
*/
public function testContentAdminPages(): void {
$this->drupalLogin($this->adminUser);
// Use an explicit changed time to ensure the expected order in the content
// admin listing. We want these to appear in the table in the same order as
// they appear in the following code, and the 'content' View has a table
// style configuration with a default sort on the 'changed' field DESC.
$time = time();
$nodes['published_page'] = $this->drupalCreateNode(['type' => 'page', 'changed' => $time--]);
$nodes['published_article'] = $this->drupalCreateNode(['type' => 'article', 'changed' => $time--]);
$nodes['unpublished_page_1'] = $this->drupalCreateNode(['type' => 'page', 'changed' => $time--, 'uid' => $this->baseUser1->id(), 'status' => 0]);
$nodes['unpublished_page_2'] = $this->drupalCreateNode(['type' => 'page', 'changed' => $time, 'uid' => $this->baseUser2->id(), 'status' => 0]);
// Verify view, edit, and delete links for any content.
$this->drupalGet('admin/content');
$this->assertSession()->statusCodeEquals(200);
$node_type_labels = $this->xpath('//td[contains(@class, "views-field-type")]');
$delta = 0;
foreach ($nodes as $node) {
$this->assertSession()->linkByHrefExists('node/' . $node->id());
$this->assertSession()->linkByHrefExists('node/' . $node->id() . '/edit');
$this->assertSession()->linkByHrefExists('node/' . $node->id() . '/delete');
// Verify that we can see the content type label.
$this->assertEquals(trim($node_type_labels[$delta]->getText()), $node->type->entity->label());
$delta++;
}
// Verify filtering by publishing status.
$this->drupalGet('admin/content', ['query' => ['status' => TRUE]]);
$this->assertSession()->linkByHrefExists('node/' . $nodes['published_page']->id() . '/edit');
$this->assertSession()->linkByHrefExists('node/' . $nodes['published_article']->id() . '/edit');
$this->assertSession()->linkByHrefNotExists('node/' . $nodes['unpublished_page_1']->id() . '/edit');
// Verify filtering by status and content type.
$this->drupalGet('admin/content', ['query' => ['status' => TRUE, 'type' => 'page']]);
$this->assertSession()->linkByHrefExists('node/' . $nodes['published_page']->id() . '/edit');
$this->assertSession()->linkByHrefNotExists('node/' . $nodes['published_article']->id() . '/edit');
// Verify no operation links are displayed for regular users.
$this->drupalLogout();
$this->drupalLogin($this->baseUser1);
$this->drupalGet('admin/content');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->linkByHrefExists('node/' . $nodes['published_page']->id());
$this->assertSession()->linkByHrefExists('node/' . $nodes['published_article']->id());
$this->assertSession()->linkByHrefNotExists('node/' . $nodes['published_page']->id() . '/edit');
$this->assertSession()->linkByHrefNotExists('node/' . $nodes['published_page']->id() . '/delete');
$this->assertSession()->linkByHrefNotExists('node/' . $nodes['published_article']->id() . '/edit');
$this->assertSession()->linkByHrefNotExists('node/' . $nodes['published_article']->id() . '/delete');
// Verify no unpublished content is displayed without permission.
$this->assertSession()->linkByHrefNotExists('node/' . $nodes['unpublished_page_1']->id());
$this->assertSession()->linkByHrefNotExists('node/' . $nodes['unpublished_page_1']->id() . '/edit');
$this->assertSession()->linkByHrefNotExists('node/' . $nodes['unpublished_page_1']->id() . '/delete');
// Verify no tableselect.
$this->assertSession()->fieldNotExists('nodes[' . $nodes['published_page']->id() . ']');
// Verify unpublished content is displayed with permission.
$this->drupalLogout();
$this->drupalLogin($this->baseUser2);
$this->drupalGet('admin/content');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->linkByHrefExists('node/' . $nodes['unpublished_page_2']->id());
// Verify no operation links are displayed.
$this->assertSession()->linkByHrefNotExists('node/' . $nodes['unpublished_page_2']->id() . '/edit');
$this->assertSession()->linkByHrefNotExists('node/' . $nodes['unpublished_page_2']->id() . '/delete');
// Verify user cannot see unpublished content of other users.
$this->assertSession()->linkByHrefNotExists('node/' . $nodes['unpublished_page_1']->id());
$this->assertSession()->linkByHrefNotExists('node/' . $nodes['unpublished_page_1']->id() . '/edit');
$this->assertSession()->linkByHrefNotExists('node/' . $nodes['unpublished_page_1']->id() . '/delete');
// Verify no tableselect.
$this->assertSession()->fieldNotExists('nodes[' . $nodes['unpublished_page_2']->id() . ']');
// Verify node access can be bypassed.
$this->drupalLogout();
$this->drupalLogin($this->baseUser3);
$this->drupalGet('admin/content');
$this->assertSession()->statusCodeEquals(200);
foreach ($nodes as $node) {
$this->assertSession()->linkByHrefExists('node/' . $node->id());
$this->assertSession()->linkByHrefExists('node/' . $node->id() . '/edit');
$this->assertSession()->linkByHrefExists('node/' . $node->id() . '/delete');
}
// Ensure that the language table column and the language exposed filter are
// not visible on monolingual sites.
$this->assertSession()->fieldNotExists('langcode');
$this->assertEquals(0, count($this->cssSelect('td.views-field-langcode')));
$this->assertEquals(0, count($this->cssSelect('td.views-field-langcode')));
}
/**
* Tests that the content overview page does not filter out nodes.
*/
public function testContentAdminPageWithLimitedContentViewer(): void {
\Drupal::service('module_installer')->install(['node_access_test']);
$this->addPrivateField(NodeType::load('page'));
node_access_rebuild();
$role_id = $this->drupalCreateRole([
'access content overview',
'view own unpublished content',
'node test view',
]);
$viewer_user = $this->drupalCreateUser(values: ['roles' => [$role_id]]);
// Create published and unpublished content authored by an administrator and
// the viewer user.
$nodes_visible = [];
$nodes_visible[] = $this->drupalCreateNode(['type' => 'page', 'uid' => $this->adminUser->id(), 'title' => 'Published page by admin']);
$nodes_visible[] = $this->drupalCreateNode(['type' => 'page', 'uid' => $viewer_user->id(), 'title' => 'Published own page']);
$nodes_visible[] = $this->drupalCreateNode(['type' => 'page', 'uid' => $this->adminUser->id(), 'title' => 'Published private page by admin', 'private' => ['value' => 1]]);
$nodes_visible[] = $this->drupalCreateNode(['type' => 'page', 'uid' => $viewer_user->id(), 'title' => 'Published own private page', 'private' => ['value' => 1]]);
$nodes_visible[] = $this->drupalCreateNode(['type' => 'page', 'uid' => $viewer_user->id(), 'title' => 'Unpublished own page', 'status' => NodeInterface::NOT_PUBLISHED]);
$nodes_visible[] = $this->drupalCreateNode(['type' => 'page', 'uid' => $viewer_user->id(), 'title' => 'Unpublished own private page', 'status' => NodeInterface::NOT_PUBLISHED, 'private' => ['value' => 1]]);
$nodes_visible[] = $this->drupalCreateNode(['type' => 'page', 'uid' => $this->adminUser->id(), 'title' => 'Unpublished private page by admin', 'status' => NodeInterface::NOT_PUBLISHED, 'private' => ['value' => 1]]);
$this->drupalLogin($viewer_user);
// Confirm the current user has limited privileges.
$admin_permissions = ['administer nodes', 'bypass node access'];
foreach ($admin_permissions as $admin_permission) {
$this->assertFalse(\Drupal::service('current_user')->hasPermission($admin_permission), sprintf('The current user does not have "%s" permission.', $admin_permission));
}
// Confirm that the nodes are visible to the less privileged user.
foreach ($nodes_visible as $node) {
self::assertTrue($node->access('view', $viewer_user));
$this->drupalGet('admin/content');
$this->assertSession()->linkByHrefExists('node/' . $node->id(), 0, sprintf('The "%s" node is visible on the admin/content page.', $node->getTitle()));
}
// Without the "node test view" permission the unpublished page of the
// admin user is not visible.
$this->drupalLogin($this->drupalCreateUser(values: [
'roles' => [
$this->drupalCreateRole([
'access content overview',
'view own unpublished content',
]),
],
]));
$unpublished_node_by_admin = $this->drupalCreateNode(['type' => 'page', 'uid' => $this->adminUser->id(), 'title' => 'Unpublished page by admin', 'status' => 0]);
self::assertFalse($unpublished_node_by_admin->access('view'));
$this->drupalGet('admin/content');
$this->assertSession()->linkByHrefNotExists('node/' . $unpublished_node_by_admin->id());
}
/**
* Tests content overview for a multilingual site.
*/
public function testContentAdminPageMultilingual(): void {
$this->drupalLogin($this->adminUser);
\Drupal::service('module_installer')->install(['language']);
ConfigurableLanguage::create([
'id' => 'es',
'label' => 'Spanish',
])->save();
$this->drupalCreateNode(['type' => 'page', 'title' => 'English title'])
->addTranslation('es')
->setTitle('Spanish title')
->save();
$this->drupalGet('admin/content');
// Ensure that both the language table column as well as the language
// exposed filter are visible on multilingual sites.
$this->assertSession()->fieldExists('langcode');
$this->assertEquals(2, count($this->cssSelect('td.views-field-langcode')));
$this->assertEquals(2, count($this->cssSelect('td.views-field-langcode')));
$this->assertSession()->pageTextContains('English title');
$this->assertSession()->pageTextContains('Spanish title');
$this->drupalGet('admin/content', ['query' => ['langcode' => '***LANGUAGE_site_default***']]);
$this->assertSession()->pageTextContains('English title');
$this->assertSession()->pageTextNotContains('Spanish title');
$this->drupalGet('admin/content', ['query' => ['langcode' => 'en']]);
$this->assertSession()->pageTextContains('English title');
$this->assertSession()->pageTextNotContains('Spanish title');
$this->drupalGet('admin/content', ['query' => ['langcode' => 'und']]);
$this->assertSession()->pageTextNotContains('English title');
$this->assertSession()->pageTextNotContains('Spanish title');
$this->drupalGet('admin/content', ['query' => ['langcode' => 'zxx']]);
$this->assertSession()->pageTextNotContains('English title');
$this->assertSession()->pageTextNotContains('Spanish title');
$this->drupalGet('admin/content', ['query' => ['langcode' => html_entity_decode('***LANGUAGE_language_interface***')]]);
$this->assertSession()->pageTextContains('English title');
$this->assertSession()->pageTextNotContains('Spanish title');
$this->drupalGet('es/admin/content', ['query' => ['langcode' => html_entity_decode('***LANGUAGE_language_interface***')]]);
$this->assertSession()->pageTextNotContains('English title');
$this->assertSession()->pageTextContains('Spanish title');
}
}

View File

@ -0,0 +1,242 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\block\Entity\Block;
use Drupal\Core\Database\Database;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Url;
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
use Drupal\user\RoleInterface;
/**
* Tests node block functionality.
*
* @group node
*/
class NodeBlockFunctionalTest extends NodeTestBase {
use AssertPageCacheContextsAndTagsTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* An administrative user for testing.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* An unprivileged user for testing.
*
* @var \Drupal\user\UserInterface
*/
protected $webUser;
/**
* {@inheritdoc}
*/
protected static $modules = ['block', 'views', 'node_block_test'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create users and test node.
$this->adminUser = $this->drupalCreateUser([
'administer content types',
'administer nodes',
'bypass node access',
'administer blocks',
'access content overview',
]);
$this->webUser = $this->drupalCreateUser([
'access content',
'create article content',
]);
}
/**
* Tests the recent comments block.
*/
public function testRecentNodeBlock(): void {
$this->drupalLogin($this->adminUser);
// Disallow anonymous users to view content.
user_role_change_permissions(RoleInterface::ANONYMOUS_ID, [
'access content' => FALSE,
]);
// Enable the recent content block with two items.
$block = $this->drupalPlaceBlock('views_block:content_recent-block_1', ['id' => 'test_block', 'items_per_page' => 2]);
// Test that block is not visible without nodes.
$this->drupalGet('');
$this->assertSession()->pageTextContains('No content available.');
// Add some test nodes.
$default_settings = ['uid' => $this->webUser->id(), 'type' => 'article'];
$node1 = $this->drupalCreateNode($default_settings);
$node2 = $this->drupalCreateNode($default_settings);
$node3 = $this->drupalCreateNode($default_settings);
// Create a second revision of node1.
$node1_revision_1 = $node1;
$node1->setNewRevision(TRUE);
$node1->setTitle('Node revision 2 title');
$node1->save();
$connection = Database::getConnection();
// Change the changed time for node so that we can test ordering.
$connection->update('node_field_data')
->fields([
'changed' => $node1->getChangedTime() + 100,
])
->condition('nid', $node2->id())
->execute();
$connection->update('node_field_data')
->fields([
'changed' => $node1->getChangedTime() + 200,
])
->condition('nid', $node3->id())
->execute();
// Test that a user without the 'access content' permission cannot
// see the block.
$this->drupalLogout();
$this->drupalGet('');
$this->assertSession()->pageTextNotContains($block->label());
// Test that only the 2 latest nodes are shown.
$this->drupalLogin($this->webUser);
$this->assertSession()->pageTextNotContains($node1->label());
$this->assertSession()->pageTextContains($node2->label());
$this->assertSession()->pageTextContains($node3->label());
// Check to make sure nodes are in the right order.
$this->assertSession()->elementExists('xpath', '//div[@id="block-test-block"]//div[@class="item-list"]/ul/li[1]/div/span/a[text() = "' . $node3->label() . '"]');
$this->drupalLogout();
$this->drupalLogin($this->adminUser);
// Set the number of recent nodes to show to 10.
$block->getPlugin()->setConfigurationValue('items_per_page', 10);
$block->save();
// Post an additional node.
$node4 = $this->drupalCreateNode($default_settings);
// Test that all four nodes are shown.
$this->drupalGet('');
$this->assertSession()->pageTextContains($node1->label());
$this->assertSession()->pageTextContains($node2->label());
$this->assertSession()->pageTextContains($node3->label());
$this->assertSession()->pageTextContains($node4->label());
$this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT, 'url.site', 'user']);
// Enable the "Powered by Drupal" block only on article nodes.
$theme = \Drupal::service('theme_handler')->getDefault();
$this->drupalGet("admin/structure/block/add/system_powered_by_block/{$theme}");
$this->assertSession()->pageTextContains('Content type');
$edit = [
'id' => $this->randomMachineName(),
'region' => 'sidebar_first',
'visibility[entity_bundle:node][bundles][article]' => 'article',
];
$this->submitForm($edit, 'Save block');
$block = Block::load($edit['id']);
$visibility = $block->getVisibility();
$this->assertTrue(isset($visibility['entity_bundle:node']['bundles']['article']), 'Visibility settings were saved to configuration');
// Create a page node.
$node5 = $this->drupalCreateNode(['uid' => $this->adminUser->id(), 'type' => 'page']);
$this->drupalLogout();
$this->drupalLogin($this->webUser);
// Verify visibility rules.
$this->drupalGet('');
$label = $block->label();
// Check that block is not displayed on the front page.
$this->assertSession()->pageTextNotContains($label);
$this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT, 'url.site', 'user', 'route']);
// Ensure that a page that does not have a node context can still be cached.
\Drupal::service('module_installer')->install(['dynamic_page_cache_test']);
$this->drupalGet(Url::fromRoute('dynamic_page_cache_test.cacheable_response'));
$this->assertSession()->responseHeaderEquals('X-Drupal-Dynamic-Cache', 'MISS');
$this->drupalGet(Url::fromRoute('dynamic_page_cache_test.cacheable_response'));
$this->assertSession()->responseHeaderEquals('X-Drupal-Dynamic-Cache', 'HIT');
\Drupal::service('module_installer')->uninstall(['dynamic_page_cache_test']);
$this->drupalGet('node/add/article');
// Check that block is displayed on the add article page.
$this->assertSession()->pageTextContains($label);
$this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'session', 'theme', 'url.path', 'url.query_args', 'user', 'route']);
// The node/add/article page is an admin path and currently uncacheable.
$this->assertSession()->responseHeaderEquals('X-Drupal-Dynamic-Cache', 'UNCACHEABLE (poor cacheability)');
$this->drupalGet('node/' . $node1->id());
// Check that block is displayed on the node page when node is of type
// 'article'.
$this->assertSession()->pageTextContains($label);
$this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT, 'url.site', 'user', 'route', 'timezone']);
$this->assertSession()->responseHeaderEquals('X-Drupal-Dynamic-Cache', 'MISS');
$this->drupalGet('node/' . $node1->id());
$this->assertSession()->responseHeaderEquals('X-Drupal-Dynamic-Cache', 'HIT');
$this->drupalGet('node/' . $node5->id());
// Check that block is not displayed on the node page when node is of type
// 'page'.
$this->assertSession()->pageTextNotContains($label);
$this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT, 'url.site', 'user', 'route', 'timezone']);
$this->assertSession()->responseHeaderEquals('X-Drupal-Dynamic-Cache', 'MISS');
$this->drupalGet('node/' . $node5->id());
$this->assertSession()->responseHeaderEquals('X-Drupal-Dynamic-Cache', 'HIT');
// Place a block to determine which revision is provided as context
// to blocks.
$this->drupalPlaceBlock('node_block_test_context', [
'context_mapping' => ['node' => '@node.node_route_context:node'],
]);
$this->drupalLogin($this->adminUser);
$this->drupalGet('node/' . $node1->id());
$this->assertSession()->pageTextContains($label);
$this->assertSession()->pageTextContains('Displaying node #' . $node1->id() . ', revision #' . $node1->getRevisionId() . ': Node revision 2 title');
// Assert that the preview page displays the block as well.
$this->drupalGet('node/' . $node1->id() . '/edit');
$this->submitForm([], 'Preview');
$this->assertSession()->pageTextContains($label);
// The previewed node object has no revision ID.
$this->assertSession()->pageTextContains('Displaying node #' . $node1->id() . ', revision #: Node revision 2 title');
// Assert that the revision page for both revisions displays the block.
$this->drupalGet(Url::fromRoute('entity.node.revision', ['node' => $node1->id(), 'node_revision' => $node1_revision_1->getRevisionId()]));
$this->assertSession()->pageTextContains($label);
$this->assertSession()->pageTextContains('Displaying node #' . $node1->id() . ', revision #' . $node1_revision_1->getRevisionId() . ': ' . $node1_revision_1->label());
$this->drupalGet(Url::fromRoute('entity.node.revision', ['node' => $node1->id(), 'node_revision' => $node1->getRevisionId()]));
$this->assertSession()->pageTextContains($label);
$this->assertSession()->pageTextContains('Displaying node #' . $node1->id() . ', revision #' . $node1->getRevisionId() . ': Node revision 2 title');
$this->drupalGet('admin/structure/block');
// Check that block is displayed on the admin/structure/block page.
$this->assertSession()->pageTextContains($label);
$this->assertSession()->linkByHrefExists($block->toUrl()->toString());
}
}

View File

@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Entity\EntityInterface;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\system\Functional\Entity\EntityWithUriCacheTagsTestBase;
/**
* Tests the Node entity's cache tags.
*
* @group node
*/
class NodeCacheTagsTest extends EntityWithUriCacheTagsTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function createEntity() {
// Create a "Camelids" node type.
NodeType::create([
'name' => 'Camelids',
'type' => 'camelids',
])->save();
// Create a "Llama" node.
$node = Node::create(['type' => 'camelids']);
$node->setTitle('Llama')
->setPublished()
->save();
return $node;
}
/**
* {@inheritdoc}
*/
protected function getAdditionalCacheContextsForEntity(EntityInterface $entity): array {
return ['timezone'];
}
/**
* {@inheritdoc}
*
* Each node must have an author.
*/
protected function getAdditionalCacheTagsForEntity(EntityInterface $node): array {
return ['user:' . $node->getOwnerId(), 'user_view'];
}
/**
* {@inheritdoc}
*/
protected function getAdditionalCacheContextsForEntityListing(): array {
return ['user.node_grants:view'];
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\node\Entity\Node;
/**
* Tests views contextual links on nodes.
*
* @group node
*/
class NodeContextualLinksTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'contextual',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests contextual links.
*/
public function testNodeContextualLinks(): void {
// Create a node item.
$node = Node::create([
'type' => 'article',
'title' => 'Unnamed',
]);
$node->save();
$user = $this->drupalCreateUser([
'administer nodes',
'access contextual links',
]);
$this->drupalLogin($user);
$this->drupalGet('node/' . $node->id());
$this->assertSession()->elementAttributeContains('css', 'div[data-contextual-id]', 'data-contextual-id', 'node:node=' . $node->id() . ':');
}
}

View File

@ -0,0 +1,336 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Database\Database;
use Drupal\Core\Language\LanguageInterface;
use Drupal\node\Entity\Node;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
/**
* Create a node and test saving it.
*
* @group node
*/
class NodeCreationTest extends NodeTestBase {
use ContentTypeCreationTrait;
/**
* Modules to install.
*
* Enable dummy module that implements hook_ENTITY_TYPE_insert() for
* exceptions (function node_test_exception_node_insert() ).
*
* @var array
*/
protected static $modules = [
'node_test_exception',
'dblog',
'test_page_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$web_user = $this->drupalCreateUser([
'create page content',
'edit own page content',
]);
$this->drupalLogin($web_user);
}
/**
* Tests the order of the node types on the add page.
*/
public function testNodeAddPageOrder(): void {
$this->createContentType(['type' => 'bundle_1', 'name' => 'Bundle 1']);
$this->createContentType(['type' => 'bundle_2', 'name' => 'Aaa Bundle 2']);
$admin_content_types = $this->drupalCreateUser(['bypass node access']);
$this->drupalLogin($admin_content_types);
$this->drupalGet('node/add');
$this->assertSession()->pageTextMatches('/Aaa Bundle 2(.*)Bundle 1/');
}
/**
* Creates a "Basic page" node and verifies its consistency in the database.
*/
public function testNodeCreation(): void {
$node_type_storage = \Drupal::entityTypeManager()->getStorage('node_type');
// Test /node/add page with only one content type.
$node_type_storage->load('article')->delete();
$this->drupalGet('node/add');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->addressEquals('node/add/page');
// Create a node.
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName(8);
$edit['body[0][value]'] = $this->randomMachineName(16);
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
// Check that the Basic page has been created.
$this->assertSession()->pageTextContains('Basic page ' . $edit['title[0][value]'] . ' has been created.');
// Verify that the creation message contains a link to a node.
$this->assertSession()->elementExists('xpath', '//div[@data-drupal-messages]//a[contains(@href, "node/")]');
// Check that the node exists in the database.
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$this->assertNotEmpty($node, 'Node found in database.');
// Verify that pages do not show submitted information by default.
$this->drupalGet('node/' . $node->id());
$this->assertSession()->pageTextNotContains($node->getOwner()->getAccountName());
$this->assertSession()->pageTextNotContains($this->container->get('date.formatter')->format($node->getCreatedTime()));
// Change the node type setting to show submitted by information.
/** @var \Drupal\node\NodeTypeInterface $node_type */
$node_type = $node_type_storage->load('page');
$node_type->setDisplaySubmitted(TRUE);
$node_type->save();
$this->drupalGet('node/' . $node->id());
$this->assertSession()->pageTextContains($node->getOwner()->getAccountName());
$this->assertSession()->pageTextContains($this->container->get('date.formatter')->format($node->getCreatedTime()));
// Check if the node revision checkbox is not rendered on node creation
// form.
$admin_user = $this->drupalCreateUser([
'administer nodes',
'create page content',
]);
$this->drupalLogin($admin_user);
$this->drupalGet('node/add/page');
$this->assertSession()->fieldNotExists('edit-revision', NULL);
// Check that a user with administer content types permission is not
// allowed to create content.
$content_types_admin = $this->drupalCreateUser(['administer content types']);
$this->drupalLogin($content_types_admin);
$this->drupalGet('node/add/page');
$this->assertSession()->statusCodeEquals(403);
}
/**
* Verifies that a transaction rolls back the failed creation.
*/
public function testFailedPageCreation(): void {
// Create a node.
$edit = [
'uid' => $this->loggedInUser->id(),
'name' => $this->loggedInUser->name,
'type' => 'page',
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
'title' => 'testing_transaction_exception',
];
try {
// An exception is generated by node_test_exception_node_insert() if the
// title is 'testing_transaction_exception'.
Node::create($edit)->save();
$this->fail('Expected exception has not been thrown.');
}
catch (\Exception) {
// Expected exception; just continue testing.
}
// Check that the node does not exist in the database.
$node = $this->drupalGetNodeByTitle($edit['title']);
$this->assertFalse($node);
// Check that the rollback error was logged.
$records = static::getWatchdogIdsForTestExceptionRollback();
// Verify that the rollback explanatory error was logged.
$this->assertNotEmpty($records);
}
/**
* Creates an unpublished node and confirms correct redirect behavior.
*/
public function testUnpublishedNodeCreation(): void {
// Set the front page to the test page.
$this->config('system.site')->set('page.front', '/test-page')->save();
// Set "Basic page" content type to be unpublished by default.
$fields = \Drupal::service('entity_field.manager')->getFieldDefinitions('node', 'page');
$fields['status']->getConfig('page')
->setDefaultValue(FALSE)
->save();
// Create a node.
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName(8);
$edit['body[0][value]'] = $this->randomMachineName(16);
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
// Check that the user was redirected to the home page.
$this->assertSession()->addressEquals('');
$this->assertSession()->pageTextContains('Test page text');
// Confirm that the node was created.
$this->assertSession()->pageTextContains('Basic page ' . $edit['title[0][value]'] . ' has been created.');
// Verify that the creation message doesn't contain a link to a node since
// the user cannot view unpublished nodes.
$this->assertSession()->elementNotExists('xpath', '//div[@data-drupal-messages]//a[contains(@href, "node/")]');
}
/**
* Creates nodes with different authored dates.
*/
public function testAuthoredDate(): void {
$now = \Drupal::time()->getRequestTime();
$admin = $this->drupalCreateUser([], NULL, TRUE);
$this->drupalLogin($admin);
// Create a node with the default creation date.
$edit = [
'title[0][value]' => $this->randomMachineName(8),
'body[0][value]' => $this->randomMachineName(16),
];
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$this->assertNotNull($node->getCreatedTime());
// Create a node with the custom creation date in the past.
$date = $now - 86400;
$edit = [
'title[0][value]' => $this->randomMachineName(8),
'body[0][value]' => $this->randomMachineName(16),
'created[0][value][date]' => date('Y-m-d', $date),
'created[0][value][time]' => date('H:i:s', $date),
];
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$this->assertEquals($date, $node->getCreatedTime());
// Create a node with the custom creation date in the future.
$date = $now + 86400;
$edit = [
'title[0][value]' => $this->randomMachineName(8),
'body[0][value]' => $this->randomMachineName(16),
'created[0][value][date]' => date('Y-m-d', $date),
'created[0][value][time]' => date('H:i:s', $date),
];
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$this->assertEquals($date, $node->getCreatedTime());
// Test an invalid date.
$edit = [
'title[0][value]' => $this->randomMachineName(8),
'body[0][value]' => $this->randomMachineName(16),
'created[0][value][date]' => '2013-13-13',
'created[0][value][time]' => '11:00:00',
];
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('The Authored on date is invalid.');
$this->assertFalse($this->drupalGetNodeByTitle($edit['title[0][value]']));
// Test an invalid time.
$edit = [
'title[0][value]' => $this->randomMachineName(8),
'body[0][value]' => $this->randomMachineName(16),
'created[0][value][date]' => '2012-01-01',
'created[0][value][time]' => '30:00:00',
];
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('The Authored on date is invalid.');
$this->assertFalse($this->drupalGetNodeByTitle($edit['title[0][value]']));
}
/**
* Tests the author autocompletion textfield.
*/
public function testAuthorAutocomplete(): void {
$admin_user = $this->drupalCreateUser([
'create page content',
]);
$this->drupalLogin($admin_user);
$this->drupalGet('node/add/page');
$this->assertSession()->statusCodeEquals(200);
// Verify that no autocompletion exists without administer nodes.
$selector = '//input[@id="edit-uid-0-target-id" and contains(@data-autocomplete-path, "/entity_reference_autocomplete/user/default")]';
$this->assertSession()->elementNotExists('xpath', $selector);
$admin_user = $this->drupalCreateUser([
'administer nodes',
'create page content',
]);
$this->drupalLogin($admin_user);
$this->drupalGet('node/add/page');
// Ensure that the user does have access to the autocompletion.
$this->assertSession()->elementsCount('xpath', $selector, 1);
}
/**
* Check node/add when no node types exist.
*/
public function testNodeAddWithoutContentTypes(): void {
$this->drupalGet('node/add');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->linkByHrefNotExists('/admin/structure/types/add');
// Test /node/add page without content types.
foreach (\Drupal::entityTypeManager()->getStorage('node_type')->loadMultiple() as $entity) {
$entity->delete();
}
$this->drupalGet('node/add');
$this->assertSession()->statusCodeEquals(403);
$admin_content_types = $this->drupalCreateUser([
'administer content types',
]);
$this->drupalLogin($admin_content_types);
$this->drupalGet('node/add');
$this->assertSession()->linkByHrefExists('/admin/structure/types/add');
}
/**
* Gets the watchdog IDs of the records with the rollback exception message.
*
* @return int[]
* Array containing the IDs of the log records with the rollback exception
* message.
*/
protected static function getWatchdogIdsForTestExceptionRollback(): array {
// PostgreSQL doesn't support bytea LIKE queries, so we need to unserialize
// first to check for the rollback exception message.
$matches = [];
$query = Database::getConnection()->select('watchdog', 'w')
->fields('w', ['wid', 'variables'])
->execute();
foreach ($query as $row) {
$variables = (array) unserialize($row->variables);
if (isset($variables['@message']) && $variables['@message'] === 'Test exception for rollback.') {
$matches[] = $row->wid;
}
}
return $matches;
}
}

View File

@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\node\NodeInterface;
use Drupal\user\UserInterface;
/**
* Tests making node base fields' displays configurable.
*
* @group node
*/
class NodeDisplayConfigurableTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['block'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Sets base fields to configurable display and check settings are respected.
*
* @param string $theme
* The name of the theme being tested.
* @param string $metadata_region
* The region of the node html content where meta data is expected.
* @param bool $field_classes
* If TRUE, check for field--name-XXX classes.
*
* @dataProvider provideThemes
*/
public function testDisplayConfigurable(string $theme, string $metadata_region, bool $field_classes): void {
\Drupal::service('theme_installer')->install([$theme]);
$this->config('system.theme')->set('default', $theme)->save();
$settings = [
'theme' => $theme,
'region' => 'content',
'weight' => -100,
];
$this->drupalPlaceBlock('page_title_block', $settings);
// Change the node type setting to show submitted by information.
$node_type = \Drupal::entityTypeManager()->getStorage('node_type')->load('page');
$node_type->setDisplaySubmitted(TRUE);
$node_type->save();
$user = $this->drupalCreateUser([
'administer nodes',
], $this->randomMachineName(14));
$this->drupalLogin($user);
$node = $this->drupalCreateNode(['uid' => $user->id()]);
$assert = $this->assertSession();
// Check the node with Drupal default non-configurable display.
$this->drupalGet($node->toUrl());
$this->assertNodeHtml($node, $user, TRUE, $metadata_region, $field_classes, $field_classes);
// Enable module to make base fields' displays configurable.
\Drupal::service('module_installer')->install(['node_display_configurable_test']);
// Configure display.
$display = EntityViewDisplay::load('node.page.default');
$display->setComponent('uid',
[
'type' => 'entity_reference_label',
'label' => 'above',
'settings' => ['link' => FALSE],
])
->removeComponent('title')
->save();
// Recheck the node with configurable display.
$this->drupalGet($node->toUrl());
$this->assertNodeHtml($node, $user, FALSE, $metadata_region, $field_classes, FALSE);
// Remove from display.
$display->removeComponent('uid')
->removeComponent('created')
->save();
$this->drupalGet($node->toUrl());
$assert->elementTextNotContains('css', 'article', $user->getAccountName());
}
/**
* Asserts that the node HTML is as expected.
*
* @param \Drupal\node\NodeInterface $node
* The node being tested.
* @param \Drupal\user\UserInterface $user
* The logged in user.
* @param bool $is_inline
* Whether the fields are rendered inline or not.
* @param string $metadata_region
* The region of the node html content where meta data is expected.
* @param bool $field_classes
* If TRUE, check for field--name-XXX classes on created/uid fields.
* @param bool $title_classes
* If TRUE, check for field--name-XXX classes on title field.
*
* @internal
*/
protected function assertNodeHtml(NodeInterface $node, UserInterface $user, bool $is_inline, string $metadata_region, bool $field_classes, bool $title_classes): void {
$assert = $this->assertSession();
$html_element = $is_inline ? 'span' : 'div';
$title_selector = 'h1 span' . ($title_classes ? '.field--name-title' : '');
$assert->elementTextContains('css', $title_selector, $node->getTitle());
// With field classes, the selector can be very specific.
if ($field_classes) {
$created_selector = 'article ' . $html_element . '.field--name-created';
$assert->elementTextContains('css', $created_selector, \Drupal::service('date.formatter')->format($node->getCreatedTime()));
}
else {
// When field classes aren't available, use HTML elements for testing.
$formatted_time = \Drupal::service('date.formatter')->format($node->getCreatedTime());
if ($is_inline) {
$created_selector = sprintf('//article//%s//%s/time[text()="%s"]', $metadata_region, $html_element, $formatted_time);
}
else {
$created_selector = sprintf('//article//%s//%s/time[text()="%s"]', $html_element, $html_element, $formatted_time);
}
$assert->elementExists('xpath', $created_selector);
}
$uid_selector = 'article ' . $html_element . ($field_classes ? '.field--name-uid' : '');
if (!$is_inline) {
$field_classes_selector = $field_classes ? "[contains(concat(' ', normalize-space(@class), ' '), ' field--name-uid ')]" : '';
$assert->elementExists('xpath', sprintf('//article//%s//*%s//%s[text()="Authored by"]', $html_element, $field_classes_selector, $html_element));
$assert->elementTextContains('css', $uid_selector, $user->getAccountName());
$assert->elementNotExists('css', "$uid_selector a");
if ($field_classes) {
$assert->elementExists('css', $created_selector);
}
}
else {
$assert->elementTextContains('css', $uid_selector . ' a', $user->getAccountName());
$assert->elementTextContains('css', 'article ' . $metadata_region, 'Submitted by');
}
}
/**
* Data provider for ::testDisplayConfigurable().
*
* @return array
* An array of test cases.
*/
public static function provideThemes() {
return [
['claro', 'footer', TRUE],
// @todo Add coverage for olivero after fixing
// https://www.drupal.org/project/drupal/issues/3215220.
// ['olivero', 'footer', TRUE],
['stable9', 'footer', FALSE],
];
}
}

View File

@ -0,0 +1,314 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\node\NodeInterface;
use Drupal\user\Entity\User;
/**
* Create a node and test node edit functionality.
*
* @group node
*/
class NodeEditFormTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A normal logged in user.
*
* @var \Drupal\user\UserInterface
*/
protected $webUser;
/**
* A user with permission to bypass content access checks.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* The node storage.
*
* @var \Drupal\node\NodeStorageInterface
*/
protected $nodeStorage;
/**
* {@inheritdoc}
*/
protected static $modules = ['block', 'node', 'datetime'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->webUser = $this->drupalCreateUser([
'edit own page content',
'create page content',
]);
$this->adminUser = $this->drupalCreateUser([
'bypass node access',
'administer nodes',
]);
$this->drupalPlaceBlock('local_tasks_block');
$this->nodeStorage = $this->container->get('entity_type.manager')->getStorage('node');
}
/**
* Checks node edit functionality.
*/
public function testNodeEdit(): void {
$this->drupalLogin($this->webUser);
$title_key = 'title[0][value]';
$body_key = 'body[0][value]';
// Create node to edit.
$edit = [];
$edit[$title_key] = $this->randomMachineName(8);
$edit[$body_key] = $this->randomMachineName(16);
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
// Check that the node exists in the database.
$node = $this->drupalGetNodeByTitle($edit[$title_key]);
$this->assertNotEmpty($node, 'Node found in database.');
// Check that "edit" link points to correct page.
$this->clickLink('Edit');
$this->assertSession()->addressEquals($node->toUrl('edit-form'));
// Check that the title and body fields are displayed with the correct
// values.
// @todo Ideally assertLink would support HTML, but it doesn't.
$this->assertSession()->responseContains('Edit');
$this->assertSession()->fieldValueEquals($title_key, $edit[$title_key]);
$this->assertSession()->fieldValueEquals($body_key, $edit[$body_key]);
// Edit the content of the node.
$edit = [];
$edit[$title_key] = $this->randomMachineName(8);
$edit[$body_key] = $this->randomMachineName(16);
// Stay on the current page, without reloading.
$this->submitForm($edit, 'Save');
// Check that the title and body fields are displayed with the updated
// values.
$this->assertSession()->pageTextContains($edit[$title_key]);
$this->assertSession()->pageTextContains($edit[$body_key]);
// Log in as a second administrator user.
$second_web_user = $this->drupalCreateUser([
'administer nodes',
'edit any page content',
]);
$this->drupalLogin($second_web_user);
// Edit the same node, creating a new revision.
$this->drupalGet("node/" . $node->id() . "/edit");
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName(8);
$edit[$body_key] = $this->randomMachineName(16);
$edit['revision'] = TRUE;
$this->submitForm($edit, 'Save');
// Ensure that the node revision has been created.
$revised_node = $this->drupalGetNodeByTitle($edit['title[0][value]'], TRUE);
$this->assertNotSame($node->getRevisionId(), $revised_node->getRevisionId(), 'A new revision has been created.');
// Ensure that the node author is preserved when it was not changed in the
// edit form.
$this->assertSame($node->getOwnerId(), $revised_node->getOwnerId(), 'The node author has been preserved.');
// Ensure that the revision authors are different since the revisions were
// made by different users.
$node_storage = \Drupal::service('entity_type.manager')->getStorage('node');
$first_node_version = $node_storage->loadRevision($node->getRevisionId());
$second_node_version = $node_storage->loadRevision($revised_node->getRevisionId());
$this->assertNotSame($first_node_version->getRevisionUser()->id(), $second_node_version->getRevisionUser()->id(), 'Each revision has a distinct user.');
// Check if the node revision checkbox is rendered on node edit form.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->fieldExists('edit-revision', NULL);
// Check that details form element opens when there are errors on child
// elements.
$this->drupalGet('node/' . $node->id() . '/edit');
$edit = [];
// This invalid date will trigger an error.
$edit['created[0][value][date]'] = $this->randomMachineName(8);
// Get the current amount of open details elements.
$open_details_elements = count($this->cssSelect('details[open="open"]'));
$this->submitForm($edit, 'Save');
// The node author details must be open.
$this->assertSession()->responseContains('<details class="node-form-author js-form-wrapper form-wrapper" data-drupal-selector="edit-author" id="edit-author" open="open">');
// Only one extra details element should now be open.
$open_details_elements++;
$this->assertCount($open_details_elements, $this->cssSelect('details[open="open"]'), 'Exactly one extra open &lt;details&gt; element found.');
// Edit the same node, save it and verify it's unpublished after unchecking
// the 'Published' boolean_checkbox and clicking 'Save'.
$this->drupalGet("node/" . $node->id() . "/edit");
$edit = ['status[value]' => FALSE];
$this->submitForm($edit, 'Save');
$node = $this->nodeStorage->load($node->id());
$this->assertFalse($node->isPublished(), 'Node is unpublished');
}
/**
* Tests changing a node's "authored by" field.
*/
public function testNodeEditAuthoredBy(): void {
$this->drupalLogin($this->adminUser);
// Create node to edit.
$body_key = 'body[0][value]';
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName(8);
$edit[$body_key] = $this->randomMachineName(16);
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
// Check that the node was authored by the currently logged in user.
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$this->assertSame($this->adminUser->id(), $node->getOwnerId(), 'Node authored by admin user.');
$this->checkVariousAuthoredByValues($node, 'uid[0][target_id]');
// Check that normal users cannot change the authored by information.
$this->drupalLogin($this->webUser);
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->fieldNotExists('uid[0][target_id]');
// Now test with the Autocomplete (Tags) field widget.
/** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display */
$form_display = \Drupal::entityTypeManager()->getStorage('entity_form_display')->load('node.page.default');
$widget = $form_display->getComponent('uid');
$widget['type'] = 'entity_reference_autocomplete_tags';
$widget['settings'] = [
'match_operator' => 'CONTAINS',
'size' => 60,
'placeholder' => '',
];
$form_display->setComponent('uid', $widget);
$form_display->save();
$this->drupalLogin($this->adminUser);
// Save the node without making any changes.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm([], 'Save');
$node = $this->nodeStorage->load($node->id());
$this->assertSame($this->webUser->id(), $node->getOwner()->id());
$this->checkVariousAuthoredByValues($node, 'uid[target_id]');
// Hide the 'authored by' field from the form.
$form_display->removeComponent('uid')->save();
// Check that saving the node without making any changes keeps the proper
// author ID.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm([], 'Save');
$node = $this->nodeStorage->load($node->id());
$this->assertSame($this->webUser->id(), $node->getOwner()->id());
}
/**
* Tests the node meta information.
*/
public function testNodeMetaInformation(): void {
// Check that regular users (i.e. without the 'administer nodes' permission)
// can not see the meta information.
$this->drupalLogin($this->webUser);
$this->drupalGet('node/add/page');
$this->assertSession()->pageTextNotContains('Not saved yet');
// Create node to edit.
$edit['title[0][value]'] = $this->randomMachineName(8);
$edit['body[0][value]'] = $this->randomMachineName(16);
$this->submitForm($edit, 'Save');
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$this->drupalGet("node/" . $node->id() . "/edit");
$this->assertSession()->pageTextNotContains('Published');
$this->assertSession()->pageTextNotContains($this->container->get('date.formatter')->format($node->getChangedTime(), 'short'));
// Check that users with the 'administer nodes' permission can see the meta
// information.
$this->drupalLogin($this->adminUser);
$this->drupalGet('node/add/page');
$this->assertSession()->pageTextContains('Not saved yet');
// Create node to edit.
$edit['title[0][value]'] = $this->randomMachineName(8);
$edit['body[0][value]'] = $this->randomMachineName(16);
$this->submitForm($edit, 'Save');
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$this->drupalGet("node/" . $node->id() . "/edit");
$this->assertSession()->pageTextContains('Published');
$this->assertSession()->pageTextContains($this->container->get('date.formatter')->format($node->getChangedTime(), 'short'));
}
/**
* Tests the node form when the author is NULL.
*/
public function testNodeFormNullAuthor(): void {
\Drupal::service('module_installer')->install(['node_no_default_author']);
$this->drupalLogin($this->adminUser);
$this->drupalGet('node/add/page');
$this->assertSession()->statusCodeEquals(200);
}
/**
* Checks that the "authored by" works correctly with various values.
*
* @param \Drupal\node\NodeInterface $node
* A node object.
* @param string $form_element_name
* The name of the form element to populate.
*/
protected function checkVariousAuthoredByValues(NodeInterface $node, $form_element_name): void {
// Try to change the 'authored by' field to an invalid user name.
$edit = [
$form_element_name => 'invalid-name',
];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('There are no users matching "invalid-name".');
// Change the authored by field to an empty string, which should assign
// authorship to the anonymous user (uid 0).
$edit[$form_element_name] = '';
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
$node = $this->nodeStorage->load($node->id());
$uid = $node->getOwnerId();
// Most SQL database drivers stringify fetches but entities are not
// necessarily stored in a SQL database. At the same time, NULL/FALSE/""
// won't do.
$this->assertTrue($uid === 0 || $uid === '0', 'Node authored by anonymous user.');
// Go back to the edit form and check that the correct value is displayed
// in the author widget.
$this->drupalGet('node/' . $node->id() . '/edit');
$anonymous_user = User::getAnonymousUser();
$expected = $anonymous_user->label() . ' (' . $anonymous_user->id() . ')';
$this->assertSession()->fieldValueEquals($form_element_name, $expected);
// Change the authored by field to another user's name (that is not
// logged in).
$edit[$form_element_name] = $this->webUser->getAccountName();
$this->submitForm($edit, 'Save');
$node = $this->nodeStorage->load($node->id());
$this->assertSame($this->webUser->id(), $node->getOwnerId(), 'Node authored by normal user.');
}
}

View File

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Cache\Cache;
use Drupal\Tests\EntityViewTrait;
/**
* Tests changing view modes for nodes.
*
* @group node
*/
class NodeEntityViewModeAlterTest extends NodeTestBase {
use EntityViewTrait;
/**
* Enable dummy module that implements hook_ENTITY_TYPE_view() for nodes.
*
* @var string[]
*/
protected static $modules = ['node_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Create a "Basic page" node and verify its consistency in the database.
*/
public function testNodeViewModeChange(): void {
$web_user = $this->drupalCreateUser([
'create page content',
'edit own page content',
]);
$this->drupalLogin($web_user);
// Create a node.
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName(8);
$edit['body[0][value]'] = 'Data that should appear only in the body for the node.';
$edit['body[0][summary]'] = 'Extra data that should appear only in the teaser for the node.';
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
// Set the flag to alter the view mode and view the node.
\Drupal::state()->set('node_test_change_view_mode', 'teaser');
Cache::invalidateTags(['rendered']);
$this->drupalGet('node/' . $node->id());
// Check that teaser mode is viewed.
$this->assertSession()->pageTextContains('Extra data that should appear only in the teaser for the node.');
// Make sure body text is not present.
$this->assertSession()->pageTextNotContains('Data that should appear only in the body for the node.');
// Test that the correct build mode has been set.
$build = $this->buildEntityView($node);
$this->assertEquals('teaser', $build['#view_mode'], 'The view mode has correctly been set to teaser.');
}
}

View File

@ -0,0 +1,149 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrl;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Tests\BrowserTestBase;
/**
* Tests multilingual support for fields.
*
* @group node
*/
class NodeFieldMultilingualTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'language'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create Basic page node type.
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
// Setup users.
$admin_user = $this->drupalCreateUser([
'administer languages',
'administer content types',
'access administration pages',
'create page content',
'edit own page content',
]);
$this->drupalLogin($admin_user);
// Add a new language.
ConfigurableLanguage::createFromLangcode('it')->save();
// Enable URL language detection and selection.
$this->config('language.types')->set('negotiation.language_interface.enabled', [
'language-url' => -8,
'language-selected' => 12,
])->save();
// Set "Basic page" content type to use multilingual support.
\Drupal::entityTypeManager()->getStorage('language_content_settings')->create([
'target_entity_type_id' => 'node',
'target_bundle' => 'page',
'language_alterable' => TRUE,
])->save();
// Make node body translatable.
$field_storage = FieldStorageConfig::loadByName('node', 'body');
$field_storage->setTranslatable(TRUE);
$field_storage->save();
}
/**
* Tests whether field languages are correctly set through the node form.
*/
public function testMultilingualNodeForm(): void {
// Create "Basic page" content.
$langcode = language_get_default_langcode('node', 'page');
$title_key = 'title[0][value]';
$title_value = $this->randomMachineName(8);
$body_key = 'body[0][value]';
$body_value = $this->randomMachineName(16);
// Create node to edit.
$edit = [];
$edit[$title_key] = $title_value;
$edit[$body_key] = $body_value;
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
// Check that the node exists in the database.
$node = $this->drupalGetNodeByTitle($edit[$title_key]);
$this->assertNotEmpty($node, 'Node found in database.');
$this->assertSame($langcode, $node->language()->getId());
$this->assertSame($body_value, $node->body->value);
// Change node language.
$langcode = 'it';
$this->drupalGet("node/{$node->id()}/edit");
$edit = [
$title_key => $this->randomMachineName(8),
'langcode[0][value]' => $langcode,
];
$this->submitForm($edit, 'Save');
$node = $this->drupalGetNodeByTitle($edit[$title_key], TRUE);
$this->assertNotEmpty($node, 'Node found in database.');
$this->assertSame($langcode, $node->language()->getId());
$this->assertSame($body_value, $node->body->value);
// Enable content language URL detection.
$this->container->get('language_negotiator')->saveConfiguration(LanguageInterface::TYPE_CONTENT, [LanguageNegotiationUrl::METHOD_ID => 0]);
// Test multilingual field language fallback logic.
$this->drupalGet("it/node/{$node->id()}");
// Verify that body is correctly displayed using Italian as requested
// language.
$this->assertSession()->pageTextContains($body_value);
$this->drupalGet("node/{$node->id()}");
// Verify that body is correctly displayed using English as requested
// language.
$this->assertSession()->pageTextContains($body_value);
}
/**
* Tests multilingual field display settings.
*/
public function testMultilingualDisplaySettings(): void {
// Create "Basic page" content.
$title_key = 'title[0][value]';
$title_value = $this->randomMachineName(8);
$body_key = 'body[0][value]';
$body_value = $this->randomMachineName(16);
// Create node to edit.
$edit = [];
$edit[$title_key] = $title_value;
$edit[$body_key] = $body_value;
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
// Check that the node exists in the database.
$node = $this->drupalGetNodeByTitle($edit[$title_key]);
$this->assertNotEmpty($node, 'Node found in database.');
// Check if node body is showed.
$this->drupalGet('node/' . $node->id());
$this->assertSession()->elementTextEquals('xpath', "//article/div//p", $node->body->value);
}
}

View File

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests updating the changed time after API and FORM entity save.
*
* @group node
*/
class NodeFormSaveChangedTimeTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'node',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A user with permissions to create and edit articles.
*
* @var \Drupal\user\UserInterface
*/
protected $authorUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a node type.
$this->drupalCreateContentType([
'type' => 'article',
'name' => 'Article',
]);
$this->authorUser = $this->drupalCreateUser([
'access content',
'create article content',
'edit any article content',
], 'author');
$this->drupalLogin($this->authorUser);
// Create one node of the above node type .
$this->drupalCreateNode([
'type' => 'article',
]);
}
/**
* Tests the changed time after API and FORM save without changes.
*/
public function testChangedTimeAfterSaveWithoutChanges(): void {
$storage = $this->container->get('entity_type.manager')->getStorage('node');
$node = $storage->load(1);
$changed_timestamp = $node->getChangedTime();
$node->save();
$node = $storage->load(1);
$this->assertEquals($changed_timestamp, $node->getChangedTime(), "The entity's changed time wasn't updated after API save without changes.");
// Ensure different save timestamps.
sleep(1);
// Save the node on the regular node edit form.
$this->drupalGet('node/1/edit');
$this->submitForm([], 'Save');
$node = $storage->load(1);
$this->assertNotEquals($node->getChangedTime(), $changed_timestamp, "The entity's changed time was updated after form save without changes.");
}
}

View File

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests help functionality for nodes.
*
* @group node
*/
class NodeHelpTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['block', 'node', 'help'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The name of the test node type to create.
*
* @var string
*/
protected $testType;
/**
* The test 'node help' text to be checked.
*
* @var string
*/
protected $testText;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create user.
$admin_user = $this->drupalCreateUser([
'administer content types',
'administer nodes',
'bypass node access',
]);
$this->drupalLogin($admin_user);
$this->drupalPlaceBlock('help_block');
$this->testType = 'type';
$this->testText = 'Help text to find on node forms.';
// Create content type.
$this->drupalCreateContentType([
'type' => $this->testType,
'help' => $this->testText,
]);
}
/**
* Verifies that help text appears on node add/edit forms.
*/
public function testNodeShowHelpText(): void {
// Check the node add form.
$this->drupalGet('node/add/' . $this->testType);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($this->testText);
// Create node and check the node edit form.
$node = $this->drupalCreateNode(['type' => $this->testType]);
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($this->testText);
}
}

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\node\NodeInterface;
/**
* Tests the output of node links (read more, add new comment, etc).
*
* @group node
*/
class NodeLinksTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['views'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests that the links can be hidden in the view display settings.
*/
public function testHideLinks(): void {
$node = $this->drupalCreateNode([
'type' => 'article',
'promote' => NodeInterface::PROMOTED,
]);
// Links are displayed by default.
$this->drupalGet('node');
$this->assertSession()->pageTextContains($node->getTitle());
$this->assertSession()->linkExists('Read more');
// Hide links.
\Drupal::service('entity_display.repository')
->getViewDisplay('node', 'article', 'teaser')
->removeComponent('links')
->save();
$this->drupalGet('node');
$this->assertSession()->pageTextContains($node->getTitle());
$this->assertSession()->linkNotExists('Read more');
}
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
/**
* Tests the node setting for displaying author and date information.
*
* @group node
*/
class NodePostSettingsTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$web_user = $this->drupalCreateUser([
'create page content',
'administer content types',
'access user profiles',
]);
$this->drupalLogin($web_user);
}
/**
* Confirms "Basic page" content type and post information is on a new node.
*/
public function testPagePostInfo(): void {
// Set "Basic page" content type to display post information.
$edit = [];
$edit['display_submitted'] = TRUE;
$this->drupalGet('admin/structure/types/manage/page');
$this->submitForm($edit, 'Save');
// Create a node.
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName(8);
$edit['body[0][value]'] = $this->randomMachineName(16);
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
// Check that the post information is displayed.
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$this->assertSession()->pageTextContainsOnce('Submitted by');
$node->delete();
// Set "Basic page" content type to display post information.
$edit = [];
$edit['display_submitted'] = FALSE;
$this->drupalGet('admin/structure/types/manage/page');
$this->submitForm($edit, 'Save');
// Create a node.
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName(8);
$edit['body[0][value]'] = $this->randomMachineName(16);
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
// Check that the post information is not displayed.
$this->assertSession()->pageTextNotContains('Submitted by');
}
}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Session\AccountInterface;
use Drupal\Tests\BrowserTestBase;
use Drupal\user\Entity\Role;
/**
* Tests the node entity preview functionality for anonymous user.
*
* @group node
*/
class NodePreviewAnonymousTest extends BrowserTestBase {
/**
* Enable node module to test on the preview.
*
* @var array
*/
protected static $modules = ['node'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create Basic page node type.
$this->drupalCreateContentType([
'type' => 'page',
'name' => 'Basic page',
'display_submitted' => FALSE,
]);
// Grant create and editing permissions to anonymous user:
$anonymous_role = Role::load(AccountInterface::ANONYMOUS_ROLE);
$anonymous_role->grantPermission('create page content');
$anonymous_role->save();
}
/**
* Checks the node preview functionality for anonymous users.
*/
public function testAnonymousPagePreview(): void {
$title_key = 'title[0][value]';
$body_key = 'body[0][value]';
// Fill in node creation form and preview node.
$edit = [
$title_key => $this->randomMachineName(),
$body_key => $this->randomMachineName(),
];
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Preview');
// Check that the preview is displaying the title, body and term.
$this->assertSession()->linkExists('Back to content editing');
$this->assertSession()->responseContains($edit[$body_key]);
$this->assertSession()->titleEquals($edit[$title_key] . ' | Drupal');
}
}

View File

@ -0,0 +1,231 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Database\Database;
use Drupal\user\Entity\User;
/**
* Tests that node access queries are properly altered by the node module.
*
* @group node
*/
class NodeQueryAlterTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node_access_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* User with permission to view content.
*
* @var \Drupal\user\Entity\User|false
*/
protected $accessUser;
/**
* User without permission to view content.
*
* @var \Drupal\user\Entity\User|false
*/
protected $noAccessUser;
/**
* User without permission to view content.
*
* @var \Drupal\user\Entity\User
*/
protected User $noAccessUser2;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
node_access_rebuild();
// Create some content.
$this->drupalCreateNode();
$this->drupalCreateNode();
$this->drupalCreateNode();
$this->drupalCreateNode();
// Create user with simple node access permission. The 'node test view'
// permission is implemented and granted by the node_access_test module.
$this->accessUser = $this->drupalCreateUser([
'access content overview',
'access content',
'node test view',
]);
$this->noAccessUser = $this->drupalCreateUser([
'access content overview',
'access content',
]);
$this->noAccessUser2 = $this->drupalCreateUser([
'access content overview',
'access content',
]);
}
/**
* Tests 'node_access' query alter, for user with access.
*
* Verifies that a non-standard table alias can be used, and that a user with
* node access can view the nodes.
*/
public function testNodeQueryAlterLowLevelWithAccess(): void {
// User with access should be able to view 4 nodes.
try {
$query = Database::getConnection()->select('node', 'n')
->fields('n');
$query->addTag('node_access');
$query->addMetaData('op', 'view');
$query->addMetaData('account', $this->accessUser);
$result = $query->execute()->fetchAll();
$this->assertCount(4, $result, 'User with access can see correct nodes');
}
catch (\Exception) {
$this->fail('Altered query is malformed');
}
}
/**
* Tests 'node_access' query alter with revision-enabled nodes.
*/
public function testNodeQueryAlterWithRevisions(): void {
// Execute a query that only deals with the 'node_revision' table.
try {
$query = \Drupal::entityTypeManager()->getStorage('node')->getQuery();
$result = $query
->accessCheck(TRUE)
->allRevisions()
->execute();
$this->assertCount(4, $result, 'User with access can see correct nodes');
}
catch (\Exception) {
$this->fail('Altered query is malformed');
}
}
/**
* Tests 'node_access' query alter, for user without access.
*
* Verifies that a non-standard table alias can be used, and that a user
* without node access cannot view the nodes.
*/
public function testNodeQueryAlterLowLevelNoAccess(): void {
// User without access should be able to view 0 nodes.
try {
$query = Database::getConnection()->select('node', 'n')
->fields('n');
$query->addTag('node_access');
$query->addMetaData('op', 'view');
$query->addMetaData('account', $this->noAccessUser);
$result = $query->execute()->fetchAll();
$this->assertCount(0, $result, 'User with no access cannot see nodes');
}
catch (\Exception) {
$this->fail('Altered query is malformed');
}
}
/**
* Tests 'node_access' query alter, for edit access.
*
* Verifies that a non-standard table alias can be used, and that a user with
* view-only node access cannot edit the nodes.
*/
public function testNodeQueryAlterLowLevelEditAccess(): void {
// User with view-only access should not be able to edit nodes.
try {
$query = Database::getConnection()->select('node', 'n')
->fields('n');
$query->addTag('node_access');
$query->addMetaData('op', 'update');
$query->addMetaData('account', $this->accessUser);
$result = $query->execute()->fetchAll();
$this->assertCount(0, $result, 'User with view-only access cannot edit nodes');
}
catch (\Exception $e) {
$this->fail($e->getMessage());
$this->fail((string) $query);
$this->fail('Altered query is malformed');
}
}
/**
* Tests 'node_access' query alter override.
*
* Verifies that node_access_view_all_nodes() is called from
* node_query_node_access_alter(). We do this by checking that a user who
* normally would not have view privileges is able to view the nodes when we
* add a record to {node_access} paired with a corresponding privilege in
* hook_node_grants().
*/
public function testNodeQueryAlterOverride(): void {
$record = [
'nid' => 0,
'gid' => 0,
'realm' => 'node_access_all',
'grant_view' => 1,
'grant_update' => 0,
'grant_delete' => 0,
];
$connection = Database::getConnection();
$connection->insert('node_access')->fields($record)->execute();
// Test that the noAccessUser still doesn't have the 'view'
// privilege after adding the node_access record.
drupal_static_reset('node_access_view_all_nodes');
try {
$query = $connection->select('node', 'n')
->fields('n');
$query->addTag('node_access');
$query->addMetaData('op', 'view');
$query->addMetaData('account', $this->noAccessUser);
$result = $query->execute()->fetchAll();
$this->assertCount(0, $result, 'User view privileges are not overridden');
}
catch (\Exception) {
$this->fail('Altered query is malformed');
}
// Have node_test_node_grants return a node_access_all privilege,
// to grant the noAccessUser 'view' access. To verify that
// node_access_view_all_nodes is properly checking the specified
// $account instead of the current user, we will log in as
// noAccessUser2.
$this->drupalLogin($this->noAccessUser2);
\Drupal::state()->set('node_access_test.no_access_uid', $this->noAccessUser->id());
drupal_static_reset('node_access_view_all_nodes');
try {
$query = $connection->select('node', 'n')
->fields('n');
$query->addTag('node_access');
$query->addMetaData('op', 'view');
$query->addMetaData('account', $this->noAccessUser);
$result = $query->execute()->fetchAll();
$this->assertCount(4, $result, 'User view privileges are overridden');
}
catch (\Exception) {
$this->fail('Altered query is malformed');
}
\Drupal::state()->delete('node_access_test.no_access_uid');
}
}

View File

@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\filter\Entity\FilterFormat;
/**
* Ensures that data added to nodes by other modules appears in RSS feeds.
*
* Create a node, enable the node_test module to ensure that extra data is
* added to the node's renderable array, then verify that the data appears on
* the site-wide RSS feed at rss.xml.
*
* @group node
*/
class NodeRSSContentTest extends NodeTestBase {
/**
* Enable a module that implements hook_node_view().
*
* @var array
*/
protected static $modules = ['node_test', 'views'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Use bypass node access permission here, because the test class uses
// hook_grants_alter() to deny access to everyone on node_access
// queries.
$user = $this->drupalCreateUser([
'bypass node access',
'access content',
'create article content',
]);
$this->drupalLogin($user);
}
/**
* Ensures that a new node includes the custom data when added to an RSS feed.
*/
public function testNodeRSSContent(): void {
// Create a node.
$node = $this->drupalCreateNode(['type' => 'article', 'promote' => 1]);
$this->drupalGet('rss.xml');
// Check that content added in 'rss' view mode appear in RSS feed.
$rss_only_content = 'Extra data that should appear only in the RSS feed for node ' . $node->id() . '.';
$this->assertSession()->responseContains($rss_only_content);
// Check that content added in view modes other than 'rss' doesn't
// appear in RSS feed.
$non_rss_content = 'Extra data that should appear everywhere except the RSS feed for node ' . $node->id() . '.';
$this->assertSession()->responseNotContains($non_rss_content);
// Check that extra RSS elements and namespaces are added to RSS feed.
$test_element = "<testElement>Value of testElement RSS element for node {$node->id()}.</testElement>";
$test_ns = 'xmlns:test="http://example.com/test-namespace"';
$this->assertSession()->responseContains($test_element);
$this->assertSession()->responseContains($test_ns);
// Check that content added in 'rss' view mode doesn't appear when
// viewing node.
$this->drupalGet('node/' . $node->id());
$this->assertSession()->responseNotContains($rss_only_content);
}
/**
* Tests relative, root-relative, protocol-relative and absolute URLs.
*/
public function testUrlHandling(): void {
// Only the plain_text text format is available by default, which escapes
// all HTML.
FilterFormat::create([
'format' => 'full_html',
'name' => 'Full HTML',
'filters' => [],
])->save();
$defaults = [
'type' => 'article',
'promote' => 1,
];
/** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
$file_url_generator = \Drupal::service('file_url_generator');
$this->drupalCreateNode($defaults + [
'body' => [
'value' => '<p><a href="' . $file_url_generator->generate('public://root-relative')->toString() . '">Root-relative URL</a></p>',
'format' => 'full_html',
],
]);
$protocol_relative_url = substr($file_url_generator->generate('public://protocol-relative')->setAbsolute()->toString(), strlen(\Drupal::request()->getScheme() . ':'));
$this->drupalCreateNode($defaults + [
'body' => [
'value' => '<p><a href="' . $protocol_relative_url . '">Protocol-relative URL</a></p>',
'format' => 'full_html',
],
]);
$absolute_url = $file_url_generator->generate('public://absolute')->setAbsolute()->toString();
$this->drupalCreateNode($defaults + [
'body' => [
'value' => '<p><a href="' . $absolute_url . '">Absolute URL</a></p>',
'format' => 'full_html',
],
]);
$this->drupalGet('rss.xml');
// Verify that root-relative URL is transformed to absolute.
$this->assertSession()->responseContains($file_url_generator->generate('public://root-relative')->setAbsolute()->toString());
// Verify that protocol-relative URL is left untouched.
$this->assertSession()->responseContains($protocol_relative_url);
// Verify that absolute URL is left untouched.
$this->assertSession()->responseContains($absolute_url);
}
}

View File

@ -0,0 +1,219 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Database\Database;
use Drupal\node\NodeInterface;
/**
* Tests global node CRUD operation permissions.
*
* @group node
*/
class NodeRevisionsAllTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A list of nodes created to be used as starting point of different tests.
*
* @var \Drupal\node\NodeInterface[]
*/
protected $nodes;
/**
* Revision logs of nodes created by the setup method.
*
* @var string[]
*/
protected $revisionLogs;
/**
* An arbitrary user for revision authoring.
*
* @var \Drupal\user\UserInterface
*/
protected $revisionUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create and log in user.
$web_user = $this->drupalCreateUser(
[
'view page revisions',
'revert page revisions',
'delete page revisions',
'edit any page content',
'delete any page content',
]
);
$this->drupalLogin($web_user);
// Create an initial node.
$node = $this->drupalCreateNode();
// Create a user for revision authoring.
// This must be different from user performing revert.
$this->revisionUser = $this->drupalCreateUser();
$nodes = [];
$logs = [];
// Get the original node.
$nodes[] = clone $node;
// Create three revisions.
$revision_count = 3;
for ($i = 0; $i < $revision_count; $i++) {
$logs[] = $node->revision_log = $this->randomMachineName(32);
$node = $this->createNodeRevision($node);
$nodes[] = clone $node;
}
$this->nodes = $nodes;
$this->revisionLogs = $logs;
}
/**
* Creates a new revision for a given node.
*
* @param \Drupal\node\NodeInterface $node
* A node object.
*
* @return \Drupal\node\NodeInterface
* A node object with up to date revision information.
*/
protected function createNodeRevision(NodeInterface $node) {
// Create revision with a random title and body and update variables.
$node->title = $this->randomMachineName();
$node->body = [
'value' => $this->randomMachineName(32),
'format' => filter_default_format(),
];
$node->setNewRevision();
// Ensure the revision author is a different user.
$node->setRevisionUserId($this->revisionUser->id());
$node->save();
return $node;
}
/**
* Checks node revision operations.
*/
public function testRevisions(): void {
$node_storage = $this->container->get('entity_type.manager')->getStorage('node');
$nodes = $this->nodes;
$logs = $this->revisionLogs;
// Get last node for simple checks.
$node = $nodes[3];
// Create and log in user.
$content_admin = $this->drupalCreateUser(
[
'view all revisions',
'revert all revisions',
'delete all revisions',
'edit any page content',
'delete any page content',
]
);
$this->drupalLogin($content_admin);
// Confirm the correct revision text appears on "view revisions" page.
$this->drupalGet("node/" . $node->id() . "/revisions/" . $node->getRevisionId() . "/view");
$this->assertSession()->pageTextContains($node->body->value);
// Confirm the correct revision log message appears on the "revisions
// overview" page.
$this->drupalGet("node/" . $node->id() . "/revisions");
foreach ($logs as $revision_log) {
$this->assertSession()->pageTextContains($revision_log);
}
// Confirm that this is the current revision.
$this->assertTrue($node->isDefaultRevision(), 'Third node revision is the current one.');
// Confirm that revisions revert properly.
$this->drupalGet("node/" . $node->id() . "/revisions/" . $nodes[1]->getRevisionId() . "/revert");
$this->submitForm([], 'Revert');
$this->assertSession()->pageTextContains("Basic page {$nodes[1]->getTitle()} has been reverted to the revision from {$this->container->get('date.formatter')->format($nodes[1]->getRevisionCreationTime())}.");
$reverted_node = $node_storage->load($node->id());
$this->assertSame($nodes[1]->body->value, $reverted_node->body->value, 'Node reverted correctly.');
// Confirm the revision author is the user performing the revert.
$this->assertSame($this->loggedInUser->id(), $reverted_node->getRevisionUserId(), 'Node revision author is user performing revert.');
// And that its not the revision author.
$this->assertNotSame($this->revisionUser->id(), $reverted_node->getRevisionUserId(), 'Node revision author is not original revision author.');
// Confirm that this is not the current version.
$node = $node_storage->loadRevision($node->getRevisionId());
$this->assertFalse($node->isDefaultRevision(), 'Third node revision is not the current one.');
// Confirm that the node can still be updated.
$this->drupalGet("node/" . $reverted_node->id() . "/edit");
$this->submitForm(['body[0][value]' => 'We are Drupal.'], 'Save');
$this->assertSession()->pageTextContains('Basic page ' . $reverted_node->getTitle() . ' has been updated.');
$this->assertSession()->pageTextContains('We are Drupal.');
// Confirm revisions delete properly.
$this->drupalGet("node/" . $node->id() . "/revisions/" . $nodes[1]->getRevisionId() . "/delete");
$this->submitForm([], 'Delete');
$this->assertSession()->pageTextContains("Revision from {$this->container->get('date.formatter')->format($nodes[1]->getRevisionCreationTime())} of Basic page {$nodes[1]->getTitle()} has been deleted.");
$nids = \Drupal::entityQuery('node')
->allRevisions()
->accessCheck(FALSE)
->condition('nid', $node->id())
->condition('vid', $nodes[1]->getRevisionId())
->execute();
$this->assertCount(0, $nids);
// Set the revision timestamp to an older date to make sure that the
// confirmation message correctly displays the stored revision date.
$old_revision_date = \Drupal::time()->getRequestTime() - 86400;
Database::getConnection()->update('node_revision')
->condition('vid', $nodes[2]->getRevisionId())
->fields([
'revision_timestamp' => $old_revision_date,
])
->execute();
$this->drupalGet("node/" . $node->id() . "/revisions/" . $nodes[2]->getRevisionId() . "/revert");
$this->submitForm([], 'Revert');
$this->assertSession()->pageTextContains("Basic page {$nodes[2]->getTitle()} has been reverted to the revision from {$this->container->get('date.formatter')->format($old_revision_date)}.");
// Create 50 more revisions in order to trigger paging on the revisions
// overview screen.
$node = $nodes[0];
for ($i = 0; $i < 50; $i++) {
$logs[] = $node->revision_log = $this->randomMachineName(32);
$node = $this->createNodeRevision($node);
$nodes[] = clone $node;
}
$this->drupalGet('node/' . $node->id() . '/revisions');
// Check that the pager exists.
$this->assertSession()->responseContains('page=1');
// Check that the last revision is displayed on the first page.
$this->assertSession()->pageTextContains(end($logs));
// Go to the second page and check that one of the initial three revisions
// is displayed.
$this->clickLink('Page 2');
$this->assertSession()->pageTextContains($logs[2]);
}
}

View File

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Url;
/**
* Tests reverting node revisions correctly sets authorship information.
*
* @group node
*/
class NodeRevisionsAuthorTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests node authorship is retained after reverting revisions.
*/
public function testNodeRevisionRevertAuthors(): void {
// Create and log in user.
$initialUser = $this->drupalCreateUser([
'view page revisions',
'revert page revisions',
'edit any page content',
]);
$initialRevisionUser = $this->drupalCreateUser();
// Third user is an author only and needs no permissions
$initialRevisionAuthor = $this->drupalCreateUser();
// Create initial node (author: $user1).
$this->drupalLogin($initialUser);
$node = $this->drupalCreateNode();
$originalRevisionId = $node->getRevisionId();
$originalBody = $node->body->value;
$originalTitle = $node->getTitle();
// Create a revision (as $initialUser) showing $initialRevisionAuthor
// as author.
$node->setRevisionLogMessage('Changed author');
$revisedTitle = $this->randomMachineName();
$node->setTitle($revisedTitle);
$revisedBody = $this->randomMachineName(32);
$node->set('body', [
'value' => $revisedBody,
'format' => filter_default_format(),
]);
$node->setOwnerId($initialRevisionAuthor->id());
$node->setRevisionUserId($initialRevisionUser->id());
$node->setNewRevision();
$node->save();
$revisedRevisionId = $node->getRevisionId();
$nodeStorage = \Drupal::entityTypeManager()->getStorage('node');
self::assertEquals($node->getOwnerId(), $initialRevisionAuthor->id());
self::assertEquals($node->getRevisionUserId(), $initialRevisionUser->id());
// Revert to the original node revision.
$this->drupalGet(Url::fromRoute('node.revision_revert_confirm', [
'node' => $node->id(),
'node_revision' => $originalRevisionId,
]));
$this->submitForm([], 'Revert');
$this->assertSession()->pageTextContains(\sprintf('Basic page %s has been reverted', $originalTitle));
// With the revert done, reload the node and verify that the authorship
// fields have reverted correctly.
$nodeStorage->resetCache([$node->id()]);
/** @var \Drupal\node\NodeInterface $revertedNode */
$revertedNode = $nodeStorage->load($node->id());
self::assertEquals($originalBody, $revertedNode->body->value);
self::assertEquals($initialUser->id(), $revertedNode->getOwnerId());
self::assertEquals($initialUser->id(), $revertedNode->getRevisionUserId());
// Revert again to the revised version and check that node author and
// revision author fields are correct.
// Revert to the original node.
$this->drupalGet(Url::fromRoute('node.revision_revert_confirm', [
'node' => $revertedNode->id(),
'node_revision' => $revisedRevisionId,
]));
$this->submitForm([], 'Revert');
$this->assertSession()->pageTextContains(\sprintf('Basic page %s has been reverted', $revisedTitle));
// With the reversion done, reload the node and verify that the
// authorship fields have reverted correctly.
$nodeStorage->resetCache([$revertedNode->id()]);
/** @var \Drupal\node\NodeInterface $re_reverted_node */
$re_reverted_node = $nodeStorage->load($revertedNode->id());
self::assertEquals($revisedBody, $re_reverted_node->body->value);
self::assertEquals($initialRevisionAuthor->id(), $re_reverted_node->getOwnerId());
// The new revision user will be the current logged in user as set in
// NodeRevisionRevertForm.
self::assertEquals($initialUser->id(), $re_reverted_node->getRevisionUserId());
}
}

View File

@ -0,0 +1,476 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Database\Database;
use Drupal\Core\Url;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
/**
* Tests per-content-type node CRUD operation permissions.
*
* @group node
*/
class NodeRevisionsTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* An array of node revisions.
*
* @var \Drupal\node\NodeInterface[]
*/
protected $nodes;
/**
* Revision log messages.
*
* @var array
*/
protected $revisionLogs;
/**
* {@inheritdoc}
*/
protected static $modules = [
'node',
'contextual',
'datetime',
'language',
'content_translation',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Enable additional languages.
ConfigurableLanguage::createFromLangcode('de')->save();
ConfigurableLanguage::createFromLangcode('it')->save();
$field_storage_definition = [
'field_name' => 'untranslatable_string_field',
'entity_type' => 'node',
'type' => 'string',
'cardinality' => 1,
'translatable' => FALSE,
];
$field_storage = FieldStorageConfig::create($field_storage_definition);
$field_storage->save();
$field_definition = [
'field_storage' => $field_storage,
'bundle' => 'page',
];
$field = FieldConfig::create($field_definition);
$field->save();
// Enable translation for page nodes.
\Drupal::service('content_translation.manager')->setEnabled('node', 'page', TRUE);
// Create and log in user.
$web_user = $this->drupalCreateUser(
[
'view page revisions',
'revert page revisions',
'delete page revisions',
'edit any page content',
'delete any page content',
'access contextual links',
'translate any entity',
'administer content types',
]
);
$this->drupalLogin($web_user);
// Create initial node.
$node = $this->drupalCreateNode();
$nodes = [];
$logs = [];
// Get original node.
$nodes[] = clone $node;
// Create three revisions.
$revision_count = 3;
for ($i = 0; $i < $revision_count; $i++) {
$logs[] = $node->revision_log = $this->randomMachineName(32);
// Create revision with a random title and body and update variables.
$node->title = $this->randomMachineName();
$node->body = [
'value' => $this->randomMachineName(32),
'format' => filter_default_format(),
];
$node->untranslatable_string_field->value = $this->randomString();
$node->setNewRevision();
// Edit the 1st and 2nd revision with a different user.
if ($i < 2) {
$editor = $this->drupalCreateUser();
$node->setRevisionUserId($editor->id());
}
else {
$node->setRevisionUserId($web_user->id());
}
$node->save();
// Make sure we get revision information.
$node = Node::load($node->id());
$nodes[] = clone $node;
}
$this->nodes = $nodes;
$this->revisionLogs = $logs;
}
/**
* Checks node revision related operations.
*/
public function testRevisions(): void {
// Access to the revision page for a node with 1 revision is allowed.
$node = $this->drupalCreateNode();
$this->drupalGet("node/" . $node->id() . "/revisions/" . $node->getRevisionId() . "/view");
$this->assertSession()->statusCodeEquals(200);
$node_storage = $this->container->get('entity_type.manager')->getStorage('node');
$nodes = $this->nodes;
$logs = $this->revisionLogs;
// Get last node for simple checks.
$node = $nodes[3];
// Confirm the correct revision text appears on "view revisions" page.
$this->drupalGet("node/" . $node->id() . "/revisions/" . $node->getRevisionId() . "/view");
$this->assertSession()->pageTextContains($node->body->value);
// Confirm the correct log message appears on "revisions overview" page.
$this->drupalGet("node/" . $node->id() . "/revisions");
foreach ($logs as $revision_log) {
$this->assertSession()->pageTextContains($revision_log);
}
// Original author, and editor names should appear on revisions overview.
$web_user = $nodes[0]->revision_uid->entity;
$this->assertSession()->pageTextContains('by ' . $web_user->getAccountName());
$editor = $nodes[2]->revision_uid->entity;
$this->assertSession()->pageTextContains('by ' . $editor->getAccountName());
// Confirm that this is the default revision.
$this->assertTrue($node->isDefaultRevision(), 'Third node revision is the default one.');
// Confirm that revisions revert properly.
$this->drupalGet("node/" . $node->id() . "/revisions/" . $nodes[1]->getRevisionid() . "/revert");
$this->submitForm([], 'Revert');
$this->assertSession()->pageTextContains("Basic page {$nodes[1]->label()} has been reverted to the revision from {$this->container->get('date.formatter')->format($nodes[1]->getRevisionCreationTime())}.");
$reverted_node = $node_storage->load($node->id());
$this->assertSame($nodes[1]->body->value, $reverted_node->body->value, 'Node reverted correctly.');
// Confirm the revision author is the user performing the revert.
$this->assertSame($this->loggedInUser->id(), $reverted_node->getRevisionUserId(), 'Node revision author is user performing revert.');
// And that its not the revision author.
$this->assertNotSame($nodes[1]->getRevisionUserId(), $reverted_node->getRevisionUserId(), 'Node revision author is not original revision author.');
// Confirm that this is not the default version.
$node = $node_storage->loadRevision($node->getRevisionId());
$this->assertFalse($node->isDefaultRevision(), 'Third node revision is not the default one.');
// Confirm revisions delete properly.
$this->drupalGet("node/" . $node->id() . "/revisions/" . $nodes[1]->getRevisionId() . "/delete");
$this->submitForm([], 'Delete');
$this->assertSession()->pageTextContains("Revision from {$this->container->get('date.formatter')->format($nodes[1]->getRevisionCreationTime())} of Basic page {$nodes[1]->label()} has been deleted.");
$connection = Database::getConnection();
$nids = \Drupal::entityQuery('node')
->accessCheck(FALSE)
->allRevisions()
->condition('nid', $node->id())
->condition('vid', $nodes[1]->getRevisionId())
->execute();
$this->assertCount(0, $nids);
// Set the revision timestamp to an older date to make sure that the
// confirmation message correctly displays the stored revision date.
$old_revision_date = \Drupal::time()->getRequestTime() - 86400;
$connection->update('node_revision')
->condition('vid', $nodes[2]->getRevisionId())
->fields([
'revision_timestamp' => $old_revision_date,
])
->execute();
$this->drupalGet("node/" . $node->id() . "/revisions/" . $nodes[2]->getRevisionId() . "/revert");
$this->submitForm([], 'Revert');
$this->assertSession()->pageTextContains("Basic page {$nodes[2]->label()} has been reverted to the revision from {$this->container->get('date.formatter')->format($old_revision_date)}.");
// Confirm user is redirected depending on the remaining revisions,
// when a revision is deleted.
$existing_revision_ids = $node_storage->revisionIds($node);
// Delete all revision except last 3.
$remaining_revision_ids = array_slice($existing_revision_ids, -3, 3);
foreach ($existing_revision_ids as $revision_id) {
if (!in_array($revision_id, $remaining_revision_ids)) {
$node_storage->deleteRevision($revision_id);
}
}
// Confirm user was redirected to revisions history page.
$this->drupalGet("node/" . $node->id() . "/revisions/" . $remaining_revision_ids[0] . "/delete");
$this->submitForm([], 'Delete');
$this->assertSession()->pageTextContains("Revisions for {$nodes[2]->label()}");
$this->assertSession()->pageTextNotContains($nodes[2]->body->value);
// Confirm user was redirected to the node page.
$this->drupalGet("node/" . $node->id() . "/revisions/" . $remaining_revision_ids[1] . "/delete");
$this->submitForm([], 'Delete');
$this->assertSession()->pageTextNotContains("Revisions for {$nodes[2]->label()}");
$this->assertSession()->pageTextContains($nodes[2]->body->value);
// Make a new revision and set it to not be default.
// This will create a new revision that is not "front facing".
$new_node_revision = clone $node;
$new_body = $this->randomMachineName();
$new_node_revision->body->value = $new_body;
// Save this as a non-default revision.
$new_node_revision->setNewRevision();
$new_node_revision->isDefaultRevision = FALSE;
$new_node_revision->save();
// Verify that revision body text is not present on default version of node.
$this->drupalGet('node/' . $node->id());
$this->assertSession()->pageTextNotContains($new_body);
// Verify that the new body text is present on the revision.
$this->drupalGet("node/" . $node->id() . "/revisions/" . $new_node_revision->getRevisionId() . "/view");
$this->assertSession()->pageTextContains($new_body);
// Verify that the non-default revision vid is greater than the default
// revision vid.
$default_revision = $connection->select('node', 'n')
->fields('n', ['vid'])
->condition('nid', $node->id())
->execute()
->fetchCol();
$default_revision_vid = $default_revision[0];
$this->assertGreaterThan($default_revision_vid, $new_node_revision->getRevisionId());
// Create an 'EN' node with a revision log message.
$node = $this->drupalCreateNode();
$node->title = 'Node title in EN';
$node->revision_log = 'Simple revision message (EN)';
$node->save();
$this->drupalGet("node/" . $node->id() . "/revisions");
// Verify revisions is accessible since the type has revisions enabled.
$this->assertSession()->statusCodeEquals(200);
// Check initial revision is shown on the node revisions overview page.
$this->assertSession()->pageTextContains('Simple revision message (EN)');
// Verify that delete operation is inaccessible for the default revision.
$this->drupalGet("node/" . $node->id() . "/revisions/" . $node->getRevisionId() . "/delete");
$this->assertSession()->statusCodeEquals(403);
// Verify that revert operation is inaccessible for the default revision.
$this->drupalGet("node/" . $node->id() . "/revisions/" . $node->getRevisionId() . "/revert");
$this->assertSession()->statusCodeEquals(403);
// Create a new revision and new log message.
$node = Node::load($node->id());
$node->body->value = 'New text (EN)';
$node->revision_log = 'New revision message (EN)';
$node->setNewRevision();
$node->save();
// Check both revisions are shown on the node revisions overview page.
$this->drupalGet("node/" . $node->id() . "/revisions");
$this->assertSession()->pageTextContains('Simple revision message (EN)');
$this->assertSession()->pageTextContains('New revision message (EN)');
// Create an 'EN' node with a revision log message.
$node = $this->drupalCreateNode();
$node->langcode = 'en';
$node->title = 'Node title in EN';
$node->revision_log = 'Simple revision message (EN)';
$node->save();
$this->drupalGet("node/" . $node->id() . "/revisions");
// Verify revisions is accessible since the type has revisions enabled.
$this->assertSession()->statusCodeEquals(200);
// Check initial revision is shown on the node revisions overview page.
$this->assertSession()->pageTextContains('Simple revision message (EN)');
// Add a translation in 'DE' and create a new revision and new log message.
$translation = $node->addTranslation('de');
$translation->title->value = 'Node title in DE';
$translation->body->value = 'New text (DE)';
$translation->revision_log = 'New revision message (DE)';
$translation->setNewRevision();
$translation->save();
// View the revision UI in 'IT', only the original node revision is shown.
$this->drupalGet("it/node/" . $node->id() . "/revisions");
$this->assertSession()->pageTextContains('Simple revision message (EN)');
$this->assertSession()->pageTextNotContains('New revision message (DE)');
// View the revision UI in 'DE', only the translated node revision is shown.
$this->drupalGet("de/node/" . $node->id() . "/revisions");
$this->assertSession()->pageTextNotContains('Simple revision message (EN)');
$this->assertSession()->pageTextContains('New revision message (DE)');
// View the revision UI in 'EN', only the original node revision is shown.
$this->drupalGet("node/" . $node->id() . "/revisions");
$this->assertSession()->pageTextContains('Simple revision message (EN)');
$this->assertSession()->pageTextNotContains('New revision message (DE)');
}
/**
* Checks that revisions are correctly saved without log messages.
*/
public function testNodeRevisionWithoutLogMessage(): void {
$node_storage = $this->container->get('entity_type.manager')->getStorage('node');
// Create a node with an initial log message.
$revision_log = $this->randomMachineName(10);
$node = $this->drupalCreateNode(['revision_log' => $revision_log]);
// Save over the same revision and explicitly provide an empty log message
// (for example, to mimic the case of a node form submitted with no text in
// the "log message" field), and check that the original log message is
// preserved.
$new_title = $this->randomMachineName(10) . 'testNodeRevisionWithoutLogMessage1';
$node = clone $node;
$node->title = $new_title;
$node->revision_log = '';
$node->setNewRevision(FALSE);
$node->save();
$this->drupalGet('node/' . $node->id());
$this->assertSession()->pageTextContains($new_title);
$node_revision = $node_storage->load($node->id());
$this->assertEquals($revision_log, $node_revision->revision_log->value, 'After an existing node revision is re-saved without a log message, the original log message is preserved.');
// Create another node with an initial revision log message.
$node = $this->drupalCreateNode(['revision_log' => $revision_log]);
// Save a new node revision without providing a log message, and check that
// this revision has an empty log message.
$new_title = $this->randomMachineName(10) . 'testNodeRevisionWithoutLogMessage2';
$node = clone $node;
$node->title = $new_title;
$node->setNewRevision();
$node->revision_log = NULL;
$node->save();
$this->drupalGet('node/' . $node->id());
$this->assertSession()->pageTextContains($new_title);
$node_revision = $node_storage->load($node->id());
$this->assertEmpty($node_revision->revision_log->value, 'After a new node revision is saved with an empty log message, the log message for the node is empty.');
}
/**
* Tests the revision translations are correctly reverted.
*/
public function testRevisionTranslationRevert(): void {
// Create a node and a few revisions.
$node = $this->drupalCreateNode(['langcode' => 'en']);
$initial_revision_id = $node->getRevisionId();
$initial_title = $node->label();
$this->createRevisions($node, 2);
// Translate the node and create a few translation revisions.
$translation = $node->addTranslation('it');
$this->createRevisions($translation, 3);
$revert_id = $node->getRevisionId();
$translated_title = $translation->label();
$untranslatable_string = $node->untranslatable_string_field->value;
// Create a new revision for the default translation in-between a series of
// translation revisions.
$this->createRevisions($node, 1);
$default_translation_title = $node->label();
// And create a few more translation revisions.
$this->createRevisions($translation, 2);
$translation_revision_id = $translation->getRevisionId();
// Now revert the a translation revision preceding the last default
// translation revision, and check that the desired value was reverted but
// the default translation value was preserved.
$revert_translation_url = Url::fromRoute('node.revision_revert_translation_confirm', [
'node' => $node->id(),
'node_revision' => $revert_id,
'langcode' => 'it',
]);
$this->drupalGet($revert_translation_url);
$this->submitForm([], 'Revert');
/** @var \Drupal\node\NodeStorage $node_storage */
$node_storage = $this->container->get('entity_type.manager')->getStorage('node');
/** @var \Drupal\node\NodeInterface $node */
$node = $node_storage->load($node->id());
$this->assertGreaterThan($translation_revision_id, $node->getRevisionId());
$this->assertEquals($default_translation_title, $node->label());
$this->assertEquals($translated_title, $node->getTranslation('it')->label());
$this->assertNotEquals($untranslatable_string, $node->untranslatable_string_field->value);
$latest_revision_id = $translation->getRevisionId();
// Now revert the a translation revision preceding the last default
// translation revision again, and check that the desired value was reverted
// but the default translation value was preserved. But in addition the
// untranslated field will be reverted as well.
$this->drupalGet($revert_translation_url);
$this->submitForm(['revert_untranslated_fields' => TRUE], 'Revert');
/** @var \Drupal\node\NodeInterface $node */
$node = $node_storage->load($node->id());
$this->assertGreaterThan($latest_revision_id, $node->getRevisionId());
$this->assertEquals($default_translation_title, $node->label());
$this->assertEquals($translated_title, $node->getTranslation('it')->label());
$this->assertEquals($untranslatable_string, $node->untranslatable_string_field->value);
$latest_revision_id = $translation->getRevisionId();
// Now revert the entity revision to the initial one where the translation
// didn't exist.
$revert_url = Url::fromRoute('node.revision_revert_confirm', [
'node' => $node->id(),
'node_revision' => $initial_revision_id,
]);
$this->drupalGet($revert_url);
$this->submitForm([], 'Revert');
/** @var \Drupal\node\NodeInterface $node */
$node = $node_storage->load($node->id());
$this->assertGreaterThan($latest_revision_id, $node->getRevisionId());
$this->assertEquals($initial_title, $node->label());
$this->assertFalse($node->hasTranslation('it'));
}
/**
* Creates a series of revisions for the specified node.
*
* @param \Drupal\node\NodeInterface $node
* The node object.
* @param int $count
* The number of revisions to be created.
*/
protected function createRevisions(NodeInterface $node, $count): void {
for ($i = 0; $i < $count; $i++) {
$node->title = $this->randomString();
$node->untranslatable_string_field->value = $this->randomString();
$node->setNewRevision(TRUE);
$node->save();
}
}
}

View File

@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\node\Entity\NodeType;
/**
* Tests the revision tab display.
*
* This test is similar to NodeRevisionsUITest except that it uses a user with
* the bypass node access permission to make sure that the revision access
* check adds correct cacheability metadata.
*
* @group node
*/
class NodeRevisionsUiBypassAccessTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* User with bypass node access permission.
*
* @var \Drupal\user\Entity\User
*/
protected $editor;
/**
* {@inheritdoc}
*/
protected static $modules = ['block'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a user.
$this->editor = $this->drupalCreateUser([
'administer nodes',
'edit any page content',
'view page revisions',
'bypass node access',
'access user profiles',
]);
}
/**
* Checks that the Revision tab is displayed correctly.
*/
public function testDisplayRevisionTab(): void {
$this->drupalPlaceBlock('local_tasks_block');
$this->drupalLogin($this->editor);
// Set page revision setting 'create new revision'. This will mean new
// revisions are created by default when the node is edited.
$type = NodeType::load('page');
$type->setNewRevision(TRUE);
$type->save();
// Create the node.
$node = $this->drupalCreateNode();
// Verify the checkbox is checked on the node edit form.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->checkboxChecked('edit-revision');
// Uncheck the create new revision checkbox and save the node.
$edit = ['revision' => FALSE];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
$this->assertSession()->addressEquals($node->toUrl());
// Verify revisions exist.
$this->assertSession()->linkExists('Revisions');
// Verify the checkbox is checked on the node edit form.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->checkboxChecked('edit-revision');
// Submit the form without changing the checkbox.
$edit = [];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
$this->assertSession()->addressEquals($node->toUrl());
$this->assertSession()->linkExists('Revisions');
}
}

View File

@ -0,0 +1,218 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Link;
use Drupal\Core\Url;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
/**
* Tests the UI for controlling node revision behavior.
*
* @group node
*/
class NodeRevisionsUiTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $modules = ['block'];
/**
* @var \Drupal\user\Entity\User
*/
protected $editor;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create users.
$this->editor = $this->drupalCreateUser([
'administer nodes',
'edit any page content',
'view page revisions',
'access user profiles',
]);
}
/**
* Checks that unchecking 'Create new revision' works when editing a node.
*/
public function testNodeFormSaveWithoutRevision(): void {
$this->drupalLogin($this->editor);
$node_storage = $this->container->get('entity_type.manager')->getStorage('node');
// Set page revision setting 'create new revision'. This will mean new
// revisions are created by default when the node is edited.
$type = NodeType::load('page');
$type->setNewRevision(TRUE);
$type->save();
// Create the node.
$node = $this->drupalCreateNode();
// Verify the checkbox is checked on the node edit form.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->checkboxChecked('edit-revision');
// Uncheck the create new revision checkbox and save the node.
$edit = ['revision' => FALSE];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
// Load the node again and check the revision is the same as before.
$node_revision = $node_storage->load($node->id(), TRUE);
$this->assertEquals($node->getRevisionId(), $node_revision->getRevisionId(), "After an existing node is saved with 'Create new revision' unchecked, a new revision is not created.");
// Verify the checkbox is checked on the node edit form.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->checkboxChecked('edit-revision');
// Submit the form without changing the checkbox.
$edit = [];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
// Load the node again and check the revision is different from before.
$node_revision = $node_storage->load($node->id());
$this->assertNotEquals($node->getRevisionId(), $node_revision->getRevisionId(), "After an existing node is saved with 'Create new revision' checked, a new revision is created.");
}
/**
* Checks HTML double escaping of revision logs.
*/
public function testNodeRevisionDoubleEscapeFix(): void {
$this->drupalLogin($this->editor);
$nodes = [];
// Create the node.
$node = $this->drupalCreateNode();
$username = [
'#theme' => 'username',
'#account' => $this->editor,
];
$editor = \Drupal::service('renderer')->renderInIsolation($username);
// Get original node.
$nodes[] = clone $node;
// Create revision with a random title and body and update variables.
$node->title = $this->randomMachineName();
$node->body = [
'value' => $this->randomMachineName(32),
'format' => filter_default_format(),
];
$node->setNewRevision();
$revision_log = 'Revision <em>message</em> with markup.';
$node->revision_log->value = $revision_log;
$node->save();
// Make sure we get revision information.
$node = Node::load($node->id());
$nodes[] = clone $node;
$this->drupalGet('node/' . $node->id() . '/revisions');
// Assert the old revision message.
$date = $this->container->get('date.formatter')->format($nodes[0]->revision_timestamp->value, 'short');
$url = new Url('entity.node.revision', ['node' => $nodes[0]->id(), 'node_revision' => $nodes[0]->getRevisionId()]);
$this->assertSession()->responseContains(Link::fromTextAndUrl($date, $url)->toString() . ' by ' . $editor);
// Assert the current revision message.
$date = $this->container->get('date.formatter')->format($nodes[1]->revision_timestamp->value, 'short');
$this->assertSession()->responseContains($nodes[1]->toLink($date)->toString() . ' by ' . $editor . '<p class="revision-log">' . $revision_log . '</p>');
}
/**
* Checks the Revisions tab.
*/
public function testNodeRevisionsTabWithDefaultRevision(): void {
$this->drupalLogin($this->editor);
// Create the node.
$node = $this->drupalCreateNode();
$storage = \Drupal::entityTypeManager()->getStorage($node->getEntityTypeId());
// Create a new revision based on the default revision.
// Revision 2.
$node = $storage->load($node->id());
$node->setNewRevision(TRUE);
$node->save();
// Revision 3.
$node = $storage->load($node->id());
$node->setNewRevision(TRUE);
$node->save();
// Revision 4.
// Trigger translation changes in order to show the revision.
$node = $storage->load($node->id());
$node->setTitle($this->randomString());
$node->isDefaultRevision(FALSE);
$node->setNewRevision(TRUE);
$node->save();
// Revision 5.
$node = $storage->load($node->id());
$node->isDefaultRevision(FALSE);
$node->setNewRevision(TRUE);
$node->save();
$node_id = $node->id();
$this->drupalGet('node/' . $node_id . '/revisions');
// Verify that the latest affected revision having been a default revision
// is displayed as the current one.
$this->assertSession()->linkByHrefNotExists('/node/' . $node_id . '/revisions/1/revert');
// The site may be installed in a subdirectory, so check if the URL is
// contained in the retrieved one.
$this->assertSession()->elementAttributeContains('xpath', '//tr[contains(@class, "revision-current")]/td/a[1]', 'href', '/node/1');
// Verify that the default revision can be an older revision than the latest
// one.
// Assert that the revisions with translations changes are shown.
$this->assertSession()->linkByHrefExists('/node/' . $node_id . '/revisions/4/revert');
// Assert that the revisions without translations changes are filtered out:
// 2, 3 and 5.
$this->assertSession()->linkByHrefNotExists('/node/' . $node_id . '/revisions/2/revert');
$this->assertSession()->linkByHrefNotExists('/node/' . $node_id . '/revisions/3/revert');
$this->assertSession()->linkByHrefNotExists('/node/' . $node_id . '/revisions/5/revert');
}
/**
* Checks the Revisions tab.
*
* Tests two 'Revisions' local tasks are not added by both Node and
* VersionHistoryLocalTasks.
*
* This can be removed after 'entity.node.version_history' local task is
* removed by https://www.drupal.org/project/drupal/issues/3153559.
*
* @covers \Drupal\node\Hook\NodeHooks1::localTasksAlter
*/
public function testNodeDuplicateRevisionsTab(): void {
$this->drupalPlaceBlock('local_tasks_block');
$this->drupalLogin($this->editor);
$node = $this->drupalCreateNode();
$this->drupalGet($node->toUrl('edit-form'));
// There must be exactly one 'Revisions' local task.
$xpath = $this->assertSession()->buildXPathQuery('//a[contains(@href, :href)]', [':href' => $node->toUrl('version-history')->toString()]);
$this->assertSession()->elementsCount('xpath', $xpath, 1);
}
}

View File

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\filter\Entity\FilterFormat;
use Drupal\views\Tests\ViewTestData;
/**
* Ensures that RSS render cache doesn't interfere with other caches.
*
* Create a node, render that node as a teaser in the RSS feed, ensure that
* the RSS teaser render doesn't contain tags from the default theme.
*
* @group node
*/
class NodeRssCacheTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node_test', 'views', 'node_test_views'];
/**
* {@inheritdoc}
*/
public static $testViews = ['test_node_article_feed'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
ViewTestData::createTestViews(static::class, ['node_test_views']);
// Use bypass node access permission here, because the test class uses
// hook_grants_alter() to deny access to everyone on node_access
// queries.
$user = $this->drupalCreateUser([
'bypass node access',
'access content',
'create article content',
]);
$this->drupalLogin($user);
}
/**
* Ensure the RSS teaser render does not interfere with default theme cache.
*/
public function testNodeRssCacheContent(): void {
// Only the plain_text text format is available by default, which escapes
// all HTML.
FilterFormat::create([
'format' => 'full_html',
'name' => 'Full HTML',
'filters' => [],
])->save();
// Create the test node.
$node = $this->drupalCreateNode([
'type' => 'article',
'promote' => 1,
'title' => 'Article Test Title',
'body' => [
'value' => '<p>Article test text.</p>',
'format' => 'full_html',
],
]);
// Render the node in the RSS feed view as a teaser.
$this->drupalGet('test-node-article-feed');
// Render the teaser normally.
$viewBuilder = $this->container->get('entity_type.manager')->getViewBuilder('node');
$build = $viewBuilder->view($node, 'teaser');
$output = $this->container->get('renderer')->renderInIsolation($build);
// Teaser must contain an "<article" tag from the stable9 theme.
$this->assertStringContainsString('<article', (string) $output);
}
}

View File

@ -0,0 +1,194 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\node\Entity\Node;
/**
* Tests $node->save() for saving content.
*
* @group node
*/
class NodeSaveTest extends NodeTestBase {
/**
* A normal logged in user.
*
* @var \Drupal\user\UserInterface
*/
protected $webUser;
/**
* {@inheritdoc}
*/
protected static $modules = ['node_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a user that is allowed to post; we'll use this to test the
// submission.
$web_user = $this->drupalCreateUser(['create article content']);
$this->drupalLogin($web_user);
$this->webUser = $web_user;
}
/**
* Checks whether custom node IDs are saved properly during an import operation.
*
* Workflow:
* - first create a piece of content
* - save the content
* - check if node exists
*/
public function testImport(): void {
// Node ID must be a number that is not in the database.
$nids = \Drupal::entityTypeManager()->getStorage('node')->getQuery()
->accessCheck(FALSE)
->sort('nid', 'DESC')
->range(0, 1)
->execute();
$max_nid = reset($nids);
$test_nid = $max_nid + mt_rand(1000, 1000000);
$title = $this->randomMachineName(8);
$node = [
'title' => $title,
'body' => [['value' => $this->randomMachineName(32)]],
'uid' => $this->webUser->id(),
'type' => 'article',
'nid' => $test_nid,
];
/** @var \Drupal\node\NodeInterface $node */
$node = Node::create($node);
$node->enforceIsNew();
$this->assertEquals($this->webUser->id(), $node->getOwnerId());
$node->save();
// Test the import.
$node_by_nid = Node::load($test_nid);
$this->assertNotEmpty($node_by_nid, 'Node load by node ID.');
$node_by_title = $this->drupalGetNodeByTitle($title);
$this->assertNotEmpty($node_by_title, 'Node load by node title.');
}
/**
* Verifies accuracy of the "created" and "changed" timestamp functionality.
*/
public function testTimestamps(): void {
// Use the default timestamps.
$edit = [
'uid' => $this->webUser->id(),
'type' => 'article',
'title' => $this->randomMachineName(8),
];
Node::create($edit)->save();
$node = $this->drupalGetNodeByTitle($edit['title']);
$this->assertEquals(\Drupal::time()->getRequestTime(), $node->getCreatedTime(), 'Creating a node sets default "created" timestamp.');
$this->assertEquals(\Drupal::time()->getRequestTime(), $node->getChangedTime(), 'Creating a node sets default "changed" timestamp.');
// Store the timestamps.
$created = $node->getCreatedTime();
$node->save();
$node = $this->drupalGetNodeByTitle($edit['title'], TRUE);
$this->assertEquals($created, $node->getCreatedTime(), 'Updating a node preserves "created" timestamp.');
// Programmatically set the timestamps using hook_ENTITY_TYPE_presave().
$node->title = 'testing_node_presave';
$node->save();
$node = $this->drupalGetNodeByTitle('testing_node_presave', TRUE);
$this->assertEquals(280299600, $node->getCreatedTime(), 'Saving a node uses "created" timestamp set in presave hook.');
$this->assertEquals(979534800, $node->getChangedTime(), 'Saving a node uses "changed" timestamp set in presave hook.');
// Programmatically set the timestamps on the node.
$edit = [
'uid' => $this->webUser->id(),
'type' => 'article',
'title' => $this->randomMachineName(8),
// Sun, 19 Nov 1978 05:00:00 GMT.
'created' => 280299600,
// Drupal 1.0 release.
'changed' => 979534800,
];
Node::create($edit)->save();
$node = $this->drupalGetNodeByTitle($edit['title']);
$this->assertEquals(280299600, $node->getCreatedTime(), 'Creating a node programmatically uses programmatically set "created" timestamp.');
$this->assertEquals(979534800, $node->getChangedTime(), 'Creating a node programmatically uses programmatically set "changed" timestamp.');
// Update the timestamps.
$node->setCreatedTime(979534800);
$node->changed = 280299600;
$node->save();
$node = $this->drupalGetNodeByTitle($edit['title'], TRUE);
$this->assertEquals(979534800, $node->getCreatedTime(), 'Updating a node uses user-set "created" timestamp.');
// Allowing setting changed timestamps is required, see
// Drupal\content_translation\ContentTranslationMetadataWrapper::setChangedTime($timestamp)
// for example.
$this->assertEquals(280299600, $node->getChangedTime(), 'Updating a node uses user-set "changed" timestamp.');
}
/**
* Tests node presave and static node load cache.
*
* This test determines changes in hook_ENTITY_TYPE_presave() and verifies
* that the static node load cache is cleared upon save.
*/
public function testDeterminingChanges(): void {
// Initial creation.
$node = Node::create([
'uid' => $this->webUser->id(),
'type' => 'article',
'title' => 'test_changes',
]);
$node->save();
// Update the node without applying changes.
$node->save();
$this->assertEquals('test_changes', $node->label(), 'No changes have been determined.');
// Apply changes.
$node->title = 'updated';
$node->save();
// The hook implementations node_test_node_presave() and
// node_test_node_update() determine changes and change the title.
$this->assertEquals('updated_presave_update', $node->label(), 'Changes have been determined.');
// Test the static node load cache to be cleared.
$node = Node::load($node->id());
$this->assertEquals('updated_presave', $node->label(), 'Static cache has been cleared.');
}
/**
* Tests saving a node on node insert.
*
* This test ensures that a node has been fully saved when
* hook_ENTITY_TYPE_insert() is invoked, so that the node can be saved again
* in a hook implementation without errors.
*
* @see node_test_node_insert()
*/
public function testNodeSaveOnInsert(): void {
// node_test_node_insert() triggers a save on insert if the title equals
// 'new'.
$node = $this->drupalCreateNode(['title' => 'new']);
$this->assertEquals('Node ' . $node->id(), $node->getTitle(), 'Node saved on node insert.');
}
}

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
/**
* Tests if the syndicate block is available.
*
* @group node
* @group legacy
*/
class NodeSyndicateBlockTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['block'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a user and log in.
$admin_user = $this->drupalCreateUser(['administer blocks']);
$this->drupalLogin($admin_user);
}
/**
* Tests that the "Syndicate" block is shown when enabled.
*/
public function testSyndicateBlock(): void {
// Place the "Syndicate" block and confirm that it is rendered.
$this->drupalPlaceBlock('node_syndicate_block', ['id' => 'test_syndicate_block', 'label' => 'Subscribe to RSS Feed']);
$this->drupalGet('');
$this->assertSession()->elementExists('xpath', '//div[@id="block-test-syndicate-block"]/*');
$this->expectDeprecation('The Syndicate block is deprecated in drupal:11.2.0 and will be removed from drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3519248');
// Verify syndicate block title.
$this->assertSession()->pageTextContains('Subscribe to RSS Feed');
// Tests the syndicate block RSS link rendered at non-front pages.
$this->drupalGet('user');
$this->clickLink('Subscribe to');
$this->assertSession()->addressEquals('rss.xml');
}
}

View File

@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Session\AccountInterface;
use Drupal\node\NodeInterface;
use Drupal\Tests\BrowserTestBase;
/**
* Sets up page and article content types.
*/
abstract class NodeTestBase extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'datetime'];
/**
* The node access control handler.
*
* @var \Drupal\Core\Entity\EntityAccessControlHandlerInterface
*/
protected $accessHandler;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create Basic page and Article node types.
if ($this->profile != 'standard') {
$this->drupalCreateContentType([
'type' => 'page',
'name' => 'Basic page',
'display_submitted' => FALSE,
]);
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
}
$this->accessHandler = \Drupal::entityTypeManager()->getAccessControlHandler('node');
}
/**
* Asserts that node access correctly grants or denies access.
*
* @param array $ops
* An associative array of the expected node access grants for the node
* and account, with each key as the name of an operation (e.g. 'view',
* 'delete') and each value a Boolean indicating whether access to that
* operation should be granted.
* @param \Drupal\node\NodeInterface $node
* The node object to check.
* @param \Drupal\Core\Session\AccountInterface $account
* The user account for which to check access.
*
* @internal
*/
public function assertNodeAccess(array $ops, NodeInterface $node, AccountInterface $account) {
foreach ($ops as $op => $result) {
$this->assertEquals($this->accessHandler->access($node, $op, $account), $result, $this->nodeAccessAssertMessage($op, $result, $node->language()->getId()));
}
}
/**
* Asserts that node create access correctly grants or denies access.
*
* @param string $bundle
* The node bundle to check access to.
* @param bool $result
* Whether access should be granted or not.
* @param \Drupal\Core\Session\AccountInterface $account
* The user account for which to check access.
* @param string|null $langcode
* (optional) The language code indicating which translation of the node
* to check. If NULL, the untranslated (fallback) access is checked.
*
* @internal
*/
public function assertNodeCreateAccess(string $bundle, bool $result, AccountInterface $account, ?string $langcode = NULL) {
$this->assertEquals($this->accessHandler->createAccess($bundle, $account, ['langcode' => $langcode]), $result, $this->nodeAccessAssertMessage('create', $result, $langcode));
}
/**
* Constructs an assert message to display which node access was tested.
*
* @param string $operation
* The operation to check access for.
* @param bool $result
* Whether access should be granted or not.
* @param string|null $langcode
* (optional) The language code indicating which translation of the node
* to check. If NULL, the untranslated (fallback) access is checked.
*
* @return string
* An assert message string which contains information in plain English
* about the node access permission test that was performed.
*/
public function nodeAccessAssertMessage($operation, $result, $langcode = NULL) {
return new FormattableMarkup(
'Node access returns @result with operation %op, language code %langcode.',
[
'@result' => $result ? 'true' : 'false',
'%op' => $operation,
'%langcode' => !empty($langcode) ? $langcode : 'empty',
]
);
}
}

View File

@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\Component\Utility\Html;
use Drupal\Tests\system\Functional\Menu\AssertBreadcrumbTrait;
/**
* Tests node title.
*
* @group node
*/
class NodeTitleTest extends NodeTestBase {
use CommentTestTrait;
use AssertBreadcrumbTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['comment', 'views', 'block'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A user with permission to bypass access content.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalPlaceBlock('system_breadcrumb_block');
$this->drupalPlaceBlock('page_title_block');
$this->adminUser = $this->drupalCreateUser([
'administer nodes',
'create article content',
'create page content',
'post comments',
]);
$this->drupalLogin($this->adminUser);
$this->addDefaultCommentField('node', 'page');
}
/**
* Creates one node and tests if the node title has the correct value.
*/
public function testNodeTitle(): void {
// Create "Basic page" content with title.
// Add the node to the frontpage so we can test if teaser links are
// clickable.
$settings = [
'title' => $this->randomMachineName(8),
'promote' => 1,
];
$node = $this->drupalCreateNode($settings);
// Test <title> tag.
$this->drupalGet('node/' . $node->id());
$this->assertSession()->elementTextEquals('xpath', '//title', $node->label() . ' | Drupal');
// Test breadcrumb in comment preview.
$this->assertBreadcrumb('comment/reply/node/' . $node->id() . '/comment', [
'' => 'Home',
'node/' . $node->id() => $node->label(),
]);
// Verify that node preview title is equal to node title.
$this->assertSession()->elementTextEquals('xpath', "//article/h2/a/span", $node->label());
// Test node title is clickable on teaser list (/node).
$this->drupalGet('node');
$this->clickLink($node->label());
// Test edge case where node title is set to 0.
$settings = [
'title' => 0,
];
$node = $this->drupalCreateNode($settings);
// Test that 0 appears as <title>.
$this->drupalGet('node/' . $node->id());
$this->assertSession()->titleEquals('0 | Drupal');
// Test that 0 appears in the template <h1>.
$this->assertSession()->elementTextEquals('xpath', '//h1', '0');
// Test edge case where node title contains special characters.
$edge_case_title = 'article\'s "title".';
$settings = [
'title' => $edge_case_title,
];
$node = $this->drupalCreateNode($settings);
// Test that the title appears as <title>. The title will be escaped on the
// the page.
$edge_case_title_escaped = Html::escape($edge_case_title);
$this->drupalGet('node/' . $node->id());
$this->assertSession()->responseContains('<title>' . $edge_case_title_escaped . ' | Drupal</title>');
// Test that the title appears as <title> when reloading the node page.
$this->drupalGet('node/' . $node->id());
$this->assertSession()->responseContains('<title>' . $edge_case_title_escaped . ' | Drupal</title>');
}
}

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Component\Utility\Html;
/**
* Tests that dangerous tags in the node title are escaped.
*
* @group node
*/
class NodeTitleXSSTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests XSS functionality with a node entity.
*/
public function testNodeTitleXSS(): void {
// Prepare a user to do the stuff.
$web_user = $this->drupalCreateUser([
'create page content',
'edit any page content',
]);
$this->drupalLogin($web_user);
$xss = '<script>alert("xss")</script>';
$title = $xss . $this->randomMachineName();
$edit = [];
$edit['title[0][value]'] = $title;
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Preview');
// Verify that harmful tags are escaped when previewing a node.
$this->assertSession()->responseNotContains($xss);
$settings = ['title' => $title];
$node = $this->drupalCreateNode($settings);
$this->drupalGet('node/' . $node->id());
// Titles should be escaped.
$this->assertSession()->responseContains('<title>' . Html::escape($title) . ' | Drupal</title>');
$this->assertSession()->responseNotContains($xss);
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->responseNotContains($xss);
}
}

View File

@ -0,0 +1,694 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Url;
use Drupal\Tests\content_translation\Functional\ContentTranslationUITestBase;
use Drupal\Tests\language\Traits\LanguageTestTrait;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\node\Entity\Node;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
/**
* Tests the Node Translation UI.
*
* @group node
*/
class NodeTranslationUITest extends ContentTranslationUITestBase {
use LanguageTestTrait;
use CommentTestTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected $defaultCacheContexts = [
'theme',
'timezone',
'url.query_args:_wrapper_format',
'url.site',
'user.permissions',
];
/**
* {@inheritdoc}
*/
protected static $modules = [
'block',
'language',
'content_translation',
'node',
'field_ui',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
$this->entityTypeId = 'node';
$this->bundle = 'article';
parent::setUp();
// Create the bundle.
$this->drupalCreateContentType(['type' => 'article', 'title' => 'Article']);
$this->doSetup();
// Ensure the help message is shown even with prefixed paths.
$this->drupalPlaceBlock('help_block', ['region' => 'content']);
// Display the language selector.
static::enableBundleTranslation('node', 'article');
$this->drupalLogin($this->translator);
}
/**
* Tests the basic translation UI.
*/
public function testTranslationUI(): void {
parent::testTranslationUI();
$this->doUninstallTest();
}
/**
* Tests changing the published status on a node without fields.
*/
public function testPublishedStatusNoFields(): void {
// Test changing the published status of an article without fields.
$this->drupalLogin($this->administrator);
// Delete all fields.
$this->drupalGet('admin/structure/types/manage/article/fields');
$this->drupalGet('admin/structure/types/manage/article/fields/node.article.' . $this->fieldName . '/delete');
$this->submitForm([], 'Delete');
// Add a node.
$default_langcode = $this->langcodes[0];
$values[$default_langcode] = ['title' => [['value' => $this->randomMachineName()]]];
$this->entityId = $this->createEntity($values[$default_langcode], $default_langcode);
$storage = $this->container->get('entity_type.manager')
->getStorage($this->entityTypeId);
$entity = $storage->load($this->entityId);
// Add a content translation.
$langcode = 'fr';
$language = ConfigurableLanguage::load($langcode);
$values[$langcode] = ['title' => [['value' => $this->randomMachineName()]]];
$entity_type_id = $entity->getEntityTypeId();
$add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
$entity->getEntityTypeId() => $entity->id(),
'source' => $default_langcode,
'target' => $langcode,
], ['language' => $language]);
$edit = $this->getEditValues($values, $langcode);
$edit['status[value]'] = FALSE;
$this->drupalGet($add_url);
$this->submitForm($edit, 'Save (this translation)');
$entity = $storage->load($this->entityId);
$translation = $entity->getTranslation($langcode);
// Make sure we unpublished the node correctly.
$this->assertFalse($this->manager->getTranslationMetadata($translation)->isPublished(), 'The translation has been correctly unpublished.');
}
/**
* {@inheritdoc}
*/
protected function getTranslatorPermissions(): array {
return array_merge(parent::getTranslatorPermissions(), ['administer nodes', "edit any $this->bundle content"]);
}
/**
* {@inheritdoc}
*/
protected function getEditorPermissions(): array {
return ['administer nodes', 'create article content'];
}
/**
* {@inheritdoc}
*/
protected function getAdministratorPermissions(): array {
return array_merge(parent::getAdministratorPermissions(), ['access administration pages', 'administer content types', 'administer node fields', 'access content overview', 'bypass node access', 'administer languages', 'administer themes', 'view the administration theme']);
}
/**
* {@inheritdoc}
*/
protected function getNewEntityValues($langcode) {
return ['title' => [['value' => $this->randomMachineName()]]] + parent::getNewEntityValues($langcode);
}
/**
* {@inheritdoc}
*/
protected function doTestPublishedStatus(): void {
$storage = $this->container->get('entity_type.manager')
->getStorage($this->entityTypeId);
$entity = $storage->load($this->entityId);
$languages = $this->container->get('language_manager')->getLanguages();
$statuses = [
TRUE,
FALSE,
];
foreach ($statuses as $index => $value) {
// (Un)publish the node translations and check that the translation
// statuses are (un)published accordingly.
foreach ($this->langcodes as $langcode) {
$options = ['language' => $languages[$langcode]];
$url = $entity->toUrl('edit-form', $options);
$this->drupalGet($url, $options);
$this->submitForm([
'status[value]' => $value,
], 'Save' . $this->getFormSubmitSuffix($entity, $langcode));
}
$entity = $storage->load($this->entityId);
foreach ($this->langcodes as $langcode) {
// The node is created as unpublished thus we switch to the published
// status first.
$status = !$index;
$translation = $entity->getTranslation($langcode);
$this->assertEquals($status, $this->manager->getTranslationMetadata($translation)->isPublished(), 'The translation has been correctly unpublished.');
}
}
}
/**
* {@inheritdoc}
*/
protected function doTestAuthoringInfo(): void {
$storage = $this->container->get('entity_type.manager')
->getStorage($this->entityTypeId);
$entity = $storage->load($this->entityId);
$languages = $this->container->get('language_manager')->getLanguages();
$values = [];
// Post different base field information for each translation.
foreach ($this->langcodes as $langcode) {
$user = $this->drupalCreateUser();
$values[$langcode] = [
'uid' => $user->id(),
'created' => \Drupal::time()->getRequestTime() - mt_rand(0, 1000),
'sticky' => (bool) mt_rand(0, 1),
'promote' => (bool) mt_rand(0, 1),
];
/** @var \Drupal\Core\Datetime\DateFormatterInterface $date_formatter */
$date_formatter = $this->container->get('date.formatter');
$edit = [
'uid[0][target_id]' => $user->getAccountName(),
'created[0][value][date]' => $date_formatter->format($values[$langcode]['created'], 'custom', 'Y-m-d'),
'created[0][value][time]' => $date_formatter->format($values[$langcode]['created'], 'custom', 'H:i:s'),
'sticky[value]' => $values[$langcode]['sticky'],
'promote[value]' => $values[$langcode]['promote'],
];
$options = ['language' => $languages[$langcode]];
$url = $entity->toUrl('edit-form', $options);
$this->drupalGet($url, $options);
$this->submitForm($edit, $this->getFormSubmitAction($entity, $langcode));
}
$entity = $storage->load($this->entityId);
foreach ($this->langcodes as $langcode) {
$translation = $entity->getTranslation($langcode);
$metadata = $this->manager->getTranslationMetadata($translation);
$this->assertEquals($values[$langcode]['uid'], $metadata->getAuthor()->id(), 'Translation author correctly stored.');
$this->assertEquals($values[$langcode]['created'], $metadata->getCreatedTime(), 'Translation date correctly stored.');
$this->assertEquals($values[$langcode]['sticky'], $translation->isSticky(), 'Sticky of Translation correctly stored.');
$this->assertEquals($values[$langcode]['promote'], $translation->isPromoted(), 'Promoted of Translation correctly stored.');
}
}
/**
* Tests that translation page inherits admin status of edit page.
*/
public function testTranslationLinkTheme(): void {
$this->drupalLogin($this->administrator);
$article = $this->drupalCreateNode(['type' => 'article', 'langcode' => $this->langcodes[0]]);
// Set up the default admin theme and use it for node editing.
$this->container->get('theme_installer')->install(['claro']);
$this->config('system.theme')->set('admin', 'claro')->save();
// Verify that translation uses the admin theme if edit is admin.
$this->drupalGet('node/' . $article->id() . '/translations');
$this->assertSession()->responseContains('core/themes/claro/css/base/elements.css');
// Turn off admin theme for editing, assert inheritance to translations.
$this->config('node.settings')->set('use_admin_theme', FALSE)->save();
// Changing node.settings:use_admin_theme requires a route rebuild.
$this->container->get('router.builder')->rebuild();
// Verify that translation uses the frontend theme if edit is frontend.
$this->drupalGet('node/' . $article->id() . '/translations');
$this->assertSession()->responseNotContains('core/themes/claro/css/base/elements.css');
// Assert presence of translation page itself (vs. DisabledBundle below).
$this->assertSession()->statusCodeEquals(200);
}
/**
* Tests that no metadata is stored for a disabled bundle.
*/
public function testDisabledBundle(): void {
// Create a bundle that does not have translation enabled.
$disabledBundle = $this->randomMachineName();
$this->drupalCreateContentType(['type' => $disabledBundle, 'name' => $disabledBundle]);
// Create a node for each bundle.
$node = $this->drupalCreateNode([
'type' => $this->bundle,
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
]);
// Make sure that nothing was inserted into the {content_translation} table.
$nids = \Drupal::entityQueryAggregate('node')
->aggregate('nid', 'COUNT')
->accessCheck(FALSE)
->condition('type', $this->bundle)
->conditionAggregate('nid', 'COUNT', 2, '>=')
->groupBy('nid')
->execute();
$this->assertCount(0, $nids);
// Ensure the translation tab is not accessible.
$this->drupalGet('node/' . $node->id() . '/translations');
$this->assertSession()->statusCodeEquals(403);
}
/**
* Tests that translations are rendered properly.
*/
public function testTranslationRendering(): void {
// Add a comment field to the article content type.
\Drupal::service('module_installer')->install(['comment']);
$this->addDefaultCommentField('node', 'article');
// Add 'post comments' permission to the authenticated role.
$role = Role::load(RoleInterface::AUTHENTICATED_ID);
$role->grantPermission('post comments')->save();
$default_langcode = $this->langcodes[0];
$values[$default_langcode] = $this->getNewEntityValues($default_langcode);
$this->entityId = $this->createEntity($values[$default_langcode], $default_langcode);
$node = \Drupal::entityTypeManager()->getStorage($this->entityTypeId)->load($this->entityId);
$node->setPromoted(TRUE);
// Create translations.
foreach (array_diff($this->langcodes, [$default_langcode]) as $langcode) {
$values[$langcode] = $this->getNewEntityValues($langcode);
$translation = $node->addTranslation($langcode, $values[$langcode]);
// Publish and promote the translation to frontpage.
$translation->setPromoted(TRUE);
$translation->setPublished();
}
$node->save();
// Test that the frontpage view displays the correct translations.
\Drupal::service('module_installer')->install(['views'], TRUE);
$this->rebuildContainer();
$this->doTestTranslations('node', $values);
// Enable the translation language renderer.
$view = \Drupal::entityTypeManager()->getStorage('view')->load('frontpage');
$display = &$view->getDisplay('default');
$display['display_options']['rendering_language'] = '***LANGUAGE_entity_translation***';
$view->save();
// Need to check from the beginning, including the base_path, in the URL
// since the pattern for the default language might be a substring of
// the strings for other languages.
$base_path = base_path();
// Check the frontpage for 'Read more' links to each translation.
// See also assertTaxonomyPage() in NodeAccessBaseTableTest.
$node_href = 'node/' . $node->id();
foreach ($this->langcodes as $langcode) {
$this->drupalGet('node', ['language' => \Drupal::languageManager()->getLanguage($langcode)]);
$num_match_found = 0;
if ($langcode == 'en') {
// Site default language does not have langcode prefix in the URL.
$expected_href = $base_path . $node_href;
}
else {
$expected_href = $base_path . $langcode . '/' . $node_href;
}
$pattern = '|^' . $expected_href . '$|';
foreach ($this->xpath("//a[text()='Read more']") as $link) {
if (preg_match($pattern, $link->getAttribute('href'), $matches) == TRUE) {
$num_match_found++;
}
}
$this->assertSame(1, $num_match_found, 'There is 1 Read more link, ' . $expected_href . ', for the ' . $langcode . ' translation of a node on the frontpage. (Found ' . $num_match_found . '.)');
}
// Check the frontpage for 'Add new comment' links that include the
// language.
$comment_form_href = 'node/' . $node->id() . '#comment-form';
foreach ($this->langcodes as $langcode) {
$this->drupalGet('node', ['language' => \Drupal::languageManager()->getLanguage($langcode)]);
$num_match_found = 0;
if ($langcode == 'en') {
// Site default language does not have langcode prefix in the URL.
$expected_href = $base_path . $comment_form_href;
}
else {
$expected_href = $base_path . $langcode . '/' . $comment_form_href;
}
$pattern = '|^' . $expected_href . '$|';
foreach ($this->xpath("//a[text()='Add new comment']") as $link) {
if (preg_match($pattern, $link->getAttribute('href'), $matches) == TRUE) {
$num_match_found++;
}
}
$this->assertSame(1, $num_match_found, 'There is 1 Add new comment link, ' . $expected_href . ', for the ' . $langcode . ' translation of a node on the frontpage. (Found ' . $num_match_found . '.)');
}
// Test that the node page displays the correct translations.
$this->doTestTranslations('node/' . $node->id(), $values);
// Test that the node page has the correct alternate hreflang links.
$this->doTestAlternateHreflangLinks($node);
}
/**
* Tests that the given path displays the correct translation values.
*
* @param string $path
* The path to be tested.
* @param array $values
* The translation values to be found.
*/
protected function doTestTranslations($path, array $values): void {
$languages = $this->container->get('language_manager')->getLanguages();
foreach ($this->langcodes as $langcode) {
$this->drupalGet($path, ['language' => $languages[$langcode]]);
$this->assertSession()->pageTextContains($values[$langcode]['title'][0]['value']);
}
}
/**
* Tests that the given path provides the correct alternate hreflang links.
*
* @param \Drupal\node\Entity\Node $node
* The node to be tested.
*/
protected function doTestAlternateHreflangLinks(Node $node): void {
$url = $node->toUrl();
$languages = $this->container->get('language_manager')->getLanguages();
$url->setAbsolute();
$urls = [];
$translations = [];
foreach ($this->langcodes as $langcode) {
$language_url = clone $url;
$urls[$langcode] = $language_url->setOption('language', $languages[$langcode]);
$translations[$langcode] = $node->getTranslation($langcode);
}
foreach ($this->langcodes as $langcode) {
// Skip unpublished translations.
if ($translations[$langcode]->isPublished()) {
$this->drupalGet($urls[$langcode]);
foreach ($urls as $alternate_langcode => $language_url) {
// Retrieve desired link elements from the HTML head.
$xpath = $this->assertSession()->buildXPathQuery('head/link[@rel = "alternate" and @href = :href and @hreflang = :hreflang]', [
':href' => $language_url->toString(),
':hreflang' => $alternate_langcode,
]);
if ($translations[$alternate_langcode]->isPublished()) {
// Verify that the node translation has the correct alternate
// hreflang link for the alternate langcode.
$this->assertSession()->elementExists('xpath', $xpath);
}
else {
// Verify that the node translation does not have an alternate
// hreflang link for the alternate langcode.
$this->assertSession()->elementNotExists('xpath', $xpath);
}
}
}
}
}
/**
* {@inheritdoc}
*/
protected function getFormSubmitSuffix(EntityInterface $entity, $langcode): string {
if (!$entity->isNew() && $entity->isTranslatable()) {
$translations = $entity->getTranslationLanguages();
if ((count($translations) > 1 || !isset($translations[$langcode])) && ($field = $entity->getFieldDefinition('status'))) {
return ' ' . ($field->isTranslatable() ? '(this translation)' : '(all translations)');
}
}
return '';
}
/**
* Tests uninstalling content_translation.
*/
protected function doUninstallTest(): void {
// Delete all the nodes so there is no data.
$nodes = Node::loadMultiple();
foreach ($nodes as $node) {
$node->delete();
}
$language_count = count(\Drupal::configFactory()->listAll('language.content_settings.'));
\Drupal::service('module_installer')->uninstall(['content_translation']);
$this->rebuildContainer();
$this->assertCount($language_count, \Drupal::configFactory()->listAll('language.content_settings.'), 'Languages have been fixed rather than deleted during content_translation uninstall.');
}
/**
* {@inheritdoc}
*/
protected function doTestTranslationEdit(): void {
$storage = $this->container->get('entity_type.manager')
->getStorage($this->entityTypeId);
$entity = $storage->load($this->entityId);
$languages = $this->container->get('language_manager')->getLanguages();
$type_name = node_get_type_label($entity);
foreach ($this->langcodes as $langcode) {
// We only want to test the title for non-english translations.
if ($langcode != 'en') {
$options = ['language' => $languages[$langcode]];
$url = $entity->toUrl('edit-form', $options);
$this->drupalGet($url);
$this->assertSession()->pageTextContains("Edit {$type_name} {$entity->getTranslation($langcode)->label()} [{$languages[$langcode]->getName()} translation]");
}
}
}
/**
* Tests that revision translations are rendered properly.
*/
public function testRevisionTranslationRendering(): void {
$storage = \Drupal::entityTypeManager()->getStorage('node');
// Create a node.
$nid = $this->createEntity(['title' => 'First rev en title'], 'en');
$node = $storage->load($nid);
$original_revision_id = $node->getRevisionId();
// Add a French translation.
$translation = $node->addTranslation('fr');
$translation->title = 'First rev fr title';
$translation->setNewRevision(FALSE);
$translation->save();
// Create a new revision.
$node->title = 'Second rev en title';
$node->setNewRevision(TRUE);
$node->save();
// Get an English view of this revision.
$original_revision = $storage->loadRevision($original_revision_id);
$original_revision_url = $original_revision->toUrl('revision')->toString();
// Should be different from regular node URL.
$this->assertNotSame($original_revision_url, $original_revision->toUrl()->toString());
$this->drupalGet($original_revision_url);
$this->assertSession()->statusCodeEquals(200);
// Contents should be in English, of correct revision.
$this->assertSession()->pageTextContains('First rev en title');
$this->assertSession()->pageTextNotContains('First rev fr title');
// Get a French view.
$url_fr = $original_revision->getTranslation('fr')->toUrl('revision')->toString();
// Should have different URL from English.
$this->assertNotSame($url_fr, $original_revision->toUrl()->toString());
$this->assertNotSame($url_fr, $original_revision_url);
$this->drupalGet($url_fr);
$this->assertSession()->statusCodeEquals(200);
// Contents should be in French, of correct revision.
$this->assertSession()->pageTextContains('First rev fr title');
$this->assertSession()->pageTextNotContains('First rev en title');
}
/**
* Tests title is not escaped (but XSS-filtered) for details form element.
*/
public function testDetailsTitleIsNotEscaped(): void {
// Create an image field.
\Drupal::service('module_installer')->install(['image']);
FieldStorageConfig::create([
'entity_type' => 'node',
'field_name' => 'field_image',
'type' => 'image',
])->save();
FieldConfig::create([
'entity_type' => 'node',
'field_name' => 'field_image',
'bundle' => 'article',
'translatable' => TRUE,
])->save();
// Make the image field a multi-value field in order to display a
// details form element.
$fieldStorage = FieldStorageConfig::loadByName('node', 'field_image');
$fieldStorage->setCardinality(2)->save();
// Enable the display of the image field.
EntityFormDisplay::load('node.article.default')
->setComponent('field_image', ['region' => 'content'])->save();
// Make the image field non-translatable.
static::setFieldTranslatable('node', 'article', 'field_image', FALSE);
// Create a node.
$nid = $this->createEntity(['title' => 'Node with multi-value image field en title'], 'en');
// Add a French translation and assert the title markup is not escaped.
$this->drupalGet("node/$nid/translations/add/en/fr");
$markup = 'Image <span class="translation-entity-all-languages">(all languages)</span>';
$this->assertSession()->assertNoEscaped($markup);
$this->assertSession()->responseContains($markup);
}
/**
* Test that when content is language neutral, it uses interface language.
*
* When language neutral content is displayed on interface language, it should
* consider the interface language for creating the content link.
*/
public function testUrlPrefixOnLanguageNeutralContent(): void {
$this->drupalLogin($this->administrator);
$neutral_langcodes = [
LanguageInterface::LANGCODE_NOT_SPECIFIED,
];
foreach ($neutral_langcodes as $langcode) {
$article = $this->drupalCreateNode(['type' => 'article', 'langcode' => $langcode]);
$this->drupalGet("{$this->langcodes[1]}/admin/content");
$this->assertSession()->linkByHrefExists("{$this->langcodes[1]}/node/{$article->id()}");
$this->drupalGet("{$this->langcodes[2]}/admin/content");
$this->assertSession()->linkByHrefExists("{$this->langcodes[2]}/node/{$article->id()}");
}
}
/**
* Test deletion of translated content from search and index rebuild.
*/
public function testSearchIndexRebuildOnTranslationDeletion(): void {
\Drupal::service('module_installer')->install(['search']);
$admin_user = $this->drupalCreateUser([
'administer site configuration',
'access administration pages',
'administer content types',
'delete content translations',
'administer content translation',
'translate any entity',
'administer search',
'search content',
'delete any article content',
]);
$this->drupalLogin($admin_user);
// Create a node.
$node = $this->drupalCreateNode([
'type' => $this->bundle,
]);
// Add a French translation.
$translation = $node->addTranslation('fr');
$translation->title = 'First rev fr title';
$translation->setNewRevision(FALSE);
$translation->save();
// Check if 1 page is listed for indexing.
$this->drupalGet('admin/config/search/pages');
$this->assertSession()->pageTextContains('There is 1 item left to index.');
// Run cron.
$this->drupalGet('admin/config/system/cron');
$this->getSession()->getPage()->pressButton('Run cron');
// Assert no items are left for indexing.
$this->drupalGet('admin/config/search/pages');
$this->assertSession()->pageTextContains('There are 0 items left to index.');
// Search for French content.
$this->drupalGet('search/node', ['query' => ['keys' => urlencode('First rev fr title')]]);
$this->assertSession()->pageTextContains('First rev fr title');
// Delete translation.
$this->drupalGet('fr/node/' . $node->id() . '/delete');
$this->getSession()->getPage()->pressButton('Delete French translation');
// Run cron.
$this->drupalGet('admin/config/system/cron');
$this->getSession()->getPage()->pressButton('Run cron');
// Assert no items are left for indexing.
$this->drupalGet('admin/config/search/pages');
$this->assertSession()->pageTextContains('There are 0 items left to index.');
// Search for French content.
$this->drupalGet('search/node', ['query' => ['keys' => urlencode('First rev fr title')]]);
$this->assertSession()->pageTextNotContains('First rev fr title');
}
/**
* Tests redirection after saving translation.
*/
public function testRedirect(): void {
$this->drupalLogin($this->administrator);
$article = $this->drupalCreateNode(['type' => 'article', 'langcode' => $this->langcodes[0]]);
$edit = [
'title[0][value]' => 'English node title',
];
$this->drupalGet('node/' . $article->id() . '/edit');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('English node title');
$this->assertEquals($this->baseUrl . '/node/' . $article->id(), $this->getSession()->getCurrentUrl());
$this->drupalGet('node/' . $article->id() . '/translations/add/' . $this->langcodes[0] . '/' . $this->langcodes[1]);
$edit = [
'title[0][value]' => 'Italian node title',
];
$this->submitForm($edit, 'Save (this translation)');
$this->assertSession()->pageTextContains('Italian node title');
$this->assertEquals($this->baseUrl . '/' . $this->langcodes[1] . '/node/' . $article->id(), $this->getSession()->getCurrentUrl());
}
}

View File

@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Language\LanguageInterface;
/**
* Tests node type initial language settings.
*
* @group node
*/
class NodeTypeInitialLanguageTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['language', 'field_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$web_user = $this->drupalCreateUser([
'bypass node access',
'administer content types',
'administer node fields',
'administer node form display',
'administer node display',
'administer languages',
]);
$this->drupalLogin($web_user);
}
/**
* Tests the node type initial language defaults, and modifies them.
*
* The default initial language must be the site's default, and the language
* locked option must be on.
*/
public function testNodeTypeInitialLanguageDefaults(): void {
$this->drupalGet('admin/structure/types/manage/article');
$this->assertTrue($this->assertSession()->optionExists('edit-language-configuration-langcode', LanguageInterface::LANGCODE_SITE_DEFAULT)->isSelected());
$this->assertSession()->checkboxNotChecked('edit-language-configuration-language-alterable');
// Tests if the language field cannot be rearranged on the manage fields
// tab.
$this->drupalGet('admin/structure/types/manage/article/fields');
$this->assertSession()->elementNotExists('xpath', '//*[@id="field-overview"]/*[@id="language"]');
// Verify that language is not selectable on node add page by default.
$this->drupalGet('node/add/article');
$this->assertSession()->fieldNotExists('langcode');
// Adds a new language and set it as default.
$edit = [
'predefined_langcode' => 'hu',
];
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm($edit, 'Add language');
$edit = [
'site_default_language' => 'hu',
];
$this->drupalGet('admin/config/regional/language');
$this->submitForm($edit, 'Save configuration');
// Tests the initial language after changing the site default language.
// First unhide the language selector.
$edit = [
'language_configuration[language_alterable]' => TRUE,
];
$this->drupalGet('admin/structure/types/manage/article');
$this->submitForm($edit, 'Save');
$this->drupalGet('node/add/article');
// Ensure that the language is selectable on node add page when language
// not hidden.
$this->assertSession()->fieldExists('langcode[0][value]');
$this->assertTrue($this->assertSession()->optionExists('edit-langcode-0-value', 'hu')->isSelected());
// Tests if the language field can be rearranged on the manage form display
// tab.
$this->drupalGet('admin/structure/types/manage/article/form-display');
$this->assertSession()->elementExists('xpath', '//*[@id="langcode"]');
// Tests if the language field can be rearranged on the manage display tab.
$this->drupalGet('admin/structure/types/manage/article/display');
$this->assertSession()->elementExists('xpath', '//*[@id="langcode"]');
// Tests if the language field is hidden by default.
$this->assertTrue($this->assertSession()->optionExists('edit-fields-langcode-region', 'hidden')->isSelected());
// Changes the initial language settings.
$edit = [
'language_configuration[langcode]' => 'en',
];
$this->drupalGet('admin/structure/types/manage/article');
$this->submitForm($edit, 'Save');
$this->drupalGet('node/add/article');
$this->assertTrue($this->assertSession()->optionExists('edit-langcode-0-value', 'en')->isSelected());
}
/**
* Tests language field visibility features.
*/
public function testLanguageFieldVisibility(): void {
// Creates a node to test Language field visibility feature.
$edit = [
'title[0][value]' => $this->randomMachineName(8),
'body[0][value]' => $this->randomMachineName(16),
];
$this->drupalGet('node/add/article');
$this->submitForm($edit, 'Save');
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$this->assertNotEmpty($node, 'Node found in database.');
// Loads node page and check if Language field is hidden by default.
$this->drupalGet('node/' . $node->id());
$this->assertSession()->elementNotExists('xpath', '//div[@id="field-language-display"]/div');
// Configures Language field formatter and check if it is saved.
$edit = [
'fields[langcode][type]' => 'language',
'fields[langcode][region]' => 'content',
];
$this->drupalGet('admin/structure/types/manage/article/display');
$this->submitForm($edit, 'Save');
$this->drupalGet('admin/structure/types/manage/article/display');
$this->assertTrue($this->assertSession()->optionExists('edit-fields-langcode-type', 'language')->isSelected());
// Loads node page and check if Language field is shown.
$this->drupalGet('node/' . $node->id());
$this->assertSession()->elementExists('xpath', '//div[@id="field-language-display"]/div');
}
}

View File

@ -0,0 +1,290 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\field\Entity\FieldConfig;
use Drupal\node\Entity\NodeType;
use Drupal\Core\Url;
use Drupal\Tests\system\Functional\Menu\AssertBreadcrumbTrait;
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
/**
* Ensures that node type functions work correctly.
*
* @group node
*/
class NodeTypeTest extends NodeTestBase {
use AssertBreadcrumbTrait;
use AssertPageCacheContextsAndTagsTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['field_ui', 'block'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Ensures that node type functions (node_type_get_*) work correctly.
*
* Load available node types and validate the returned data.
*/
public function testNodeTypeGetFunctions(): void {
$node_types = NodeType::loadMultiple();
$node_names = node_type_get_names();
$this->assertTrue(isset($node_types['article']), 'Node type article is available.');
$this->assertTrue(isset($node_types['page']), 'Node type basic page is available.');
$this->assertEquals($node_names['article'], $node_types['article']->label(), 'Correct node type base has been returned.');
$article = NodeType::load('article');
$this->assertEquals($node_types['article'], $article, 'Correct node type has been returned.');
$this->assertEquals($node_types['article']->label(), $article->label(), 'Correct node type name has been returned.');
}
/**
* Tests creating a content type programmatically and via a form.
*/
public function testNodeTypeCreation(): void {
// Create a content type programmatically.
$type = $this->drupalCreateContentType();
$type_exists = (bool) NodeType::load($type->id());
$this->assertTrue($type_exists, 'The new content type has been created in the database.');
// Log in a test user.
$web_user = $this->drupalCreateUser([
'create ' . $type->label() . ' content',
]);
$this->drupalLogin($web_user);
$this->drupalGet('node/add/' . $type->id());
$this->assertSession()->statusCodeEquals(200);
// Create a content type via the user interface.
$web_user = $this->drupalCreateUser([
'bypass node access',
'administer content types',
]);
$this->drupalLogin($web_user);
$this->drupalGet('node/add');
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'config:node_type_list');
$this->assertCacheContext('user.permissions');
$elements = $this->cssSelect('dl dt');
$this->assertCount(3, $elements);
$edit = [
'name' => 'foo',
'title_label' => 'title for foo',
'type' => 'foo',
];
$this->drupalGet('admin/structure/types/add');
$this->submitForm($edit, 'Save and manage fields');
// Asserts that form submit redirects to the expected manage fields page.
$this->assertSession()->addressEquals('admin/structure/types/manage/' . $edit['name'] . '/fields');
$type_exists = (bool) NodeType::load('foo');
$this->assertTrue($type_exists, 'The new content type has been created in the database.');
$this->drupalGet('node/add');
$elements = $this->cssSelect('dl dt');
$this->assertCount(4, $elements);
}
/**
* Tests editing a node type using the UI.
*/
public function testNodeTypeEditing(): void {
$assert = $this->assertSession();
$this->drupalPlaceBlock('system_breadcrumb_block');
$web_user = $this->drupalCreateUser([
'bypass node access',
'administer content types',
'administer node fields',
]);
$this->drupalLogin($web_user);
$field = FieldConfig::loadByName('node', 'page', 'body');
$this->assertEquals('Body', $field->getLabel(), 'Body field was found.');
// Verify that title and body fields are displayed.
$this->drupalGet('node/add/page');
$assert->pageTextContains('Title');
$assert->pageTextContains('Body');
// Rename the title field.
$edit = [
'title_label' => 'Foo',
];
$this->drupalGet('admin/structure/types/manage/page');
$this->submitForm($edit, 'Save');
$this->drupalGet('node/add/page');
$assert->pageTextContains('Foo');
$assert->pageTextNotContains('Title');
// Change the name and the description.
$edit = [
'name' => 'Bar',
'description' => 'Lorem ipsum.',
];
$this->drupalGet('admin/structure/types/manage/page');
$this->submitForm($edit, 'Save');
$this->drupalGet('node/add');
$assert->pageTextContains('Bar');
$assert->pageTextContains('Lorem ipsum');
$this->clickLink('Bar');
$assert->pageTextContains('Foo');
$assert->pageTextContains('Body');
// Change the name through the API
/** @var \Drupal\node\NodeTypeInterface $node_type */
$node_type = NodeType::load('page');
$node_type->set('name', 'NewBar');
$node_type->save();
/** @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info */
$bundle_info = \Drupal::service('entity_type.bundle.info');
$node_bundles = $bundle_info->getBundleInfo('node');
$this->assertEquals('NewBar', $node_bundles['page']['label'], 'Node type bundle cache is updated');
// Remove the body field.
$this->drupalGet('admin/structure/types/manage/page/fields/node.page.body/delete');
$this->submitForm([], 'Delete');
// Resave the settings for this type.
$this->drupalGet('admin/structure/types/manage/page');
$this->submitForm([], 'Save');
$front_page_path = Url::fromRoute('<front>')->toString();
$this->assertBreadcrumb('admin/structure/types/manage/page/fields', [
$front_page_path => 'Home',
'admin/structure/types' => 'Content types',
'admin/structure/types/manage/page' => 'NewBar',
]);
// Check that the body field doesn't exist.
$this->drupalGet('node/add/page');
$assert->pageTextNotContains('Body');
}
/**
* Tests deleting a content type that still has content.
*/
public function testNodeTypeDeletion(): void {
$this->drupalPlaceBlock('page_title_block');
// Create a content type programmatically.
$type = $this->drupalCreateContentType();
// Log in a test user.
$web_user = $this->drupalCreateUser([
'bypass node access',
'administer content types',
]);
$this->drupalLogin($web_user);
// Add a new node of this type.
$node = $this->drupalCreateNode(['type' => $type->id()]);
// Attempt to delete the content type, which should not be allowed.
$this->drupalGet('admin/structure/types/manage/' . $type->label() . '/delete');
$this->assertSession()->pageTextContains("{$type->label()} is used by 1 piece of content on your site. You can not remove this content type until you have removed all of the {$type->label()} content.");
$this->assertSession()->pageTextNotContains('This action cannot be undone.');
// Delete the node.
$node->delete();
// Attempt to delete the content type, which should now be allowed.
$this->drupalGet('admin/structure/types/manage/' . $type->label() . '/delete');
$this->assertSession()->pageTextContains("Are you sure you want to delete the content type {$type->label()}?");
$this->assertSession()->pageTextContains('This action cannot be undone.');
// Test that a locked node type could not be deleted.
$this->container->get('module_installer')->install(['node_test_config']);
// Lock the default node type.
$locked = \Drupal::state()->get('node.type.locked');
$locked['default'] = 'default';
\Drupal::state()->set('node.type.locked', $locked);
// Call to flush all caches after installing the node_test_config module in
// the same way installing a module through the UI does.
$this->resetAll();
$this->drupalGet('admin/structure/types/manage/default');
$this->assertSession()->linkNotExists('Delete');
$this->drupalGet('admin/structure/types/manage/default/delete');
$this->assertSession()->statusCodeEquals(403);
$this->container->get('module_installer')->uninstall(['node_test_config']);
$this->container = \Drupal::getContainer();
unset($locked['default']);
\Drupal::state()->set('node.type.locked', $locked);
$this->drupalGet('admin/structure/types/manage/default');
$this->clickLink('Delete');
$this->assertSession()->statusCodeEquals(200);
$this->submitForm([], 'Delete');
$this->assertFalse((bool) NodeType::load('default'), 'Node type with machine default deleted.');
}
/**
* Tests operations from Field UI and User modules for content types.
*/
public function testNodeTypeOperations(): void {
// Create an admin user who can only manage node fields.
$admin_user_1 = $this->drupalCreateUser([
'administer content types',
'administer node fields',
'administer permissions',
]);
$this->drupalLogin($admin_user_1);
// Test that the user only sees the actions available to them.
$this->drupalGet('admin/structure/types');
$this->assertSession()->linkByHrefExists('admin/structure/types/manage/article/fields');
$this->assertSession()->linkByHrefExists('admin/structure/types/manage/article/permissions');
$this->assertSession()->linkByHrefNotExists('admin/structure/types/manage/article/display');
// Create another admin user who can manage node fields display.
$admin_user_2 = $this->drupalCreateUser([
'administer content types',
'administer node display',
]);
$this->drupalLogin($admin_user_2);
// Test that the user only sees the actions available to them.
$this->drupalGet('admin/structure/types');
$this->assertSession()->linkByHrefNotExists('admin/structure/types/manage/article/fields');
$this->assertSession()->linkByHrefNotExists('admin/structure/types/manage/article/permissions');
$this->assertSession()->linkByHrefExists('admin/structure/types/manage/article/display');
}
/**
* Tests for when there are no content types defined.
*/
public function testNodeTypeNoContentType(): void {
/** @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info */
$bundle_info = \Drupal::service('entity_type.bundle.info');
$this->assertCount(2, $bundle_info->getBundleInfo('node'), 'The bundle information service has 2 bundles for the Node entity type.');
$web_user = $this->drupalCreateUser(['administer content types']);
$this->drupalLogin($web_user);
// Delete 'article' bundle.
$this->drupalGet('admin/structure/types/manage/article/delete');
$this->submitForm([], 'Delete');
// Delete 'page' bundle.
$this->drupalGet('admin/structure/types/manage/page/delete');
$this->submitForm([], 'Delete');
// Navigate to content type administration screen
$this->drupalGet('admin/structure/types');
$this->assertSession()->pageTextContains("No content types available. Add content type.");
$this->assertSession()->linkExists("Add content type");
$this->assertSession()->linkByHrefExists(Url::fromRoute('node.type_add')->toString());
$bundle_info->clearCachedBundles();
$this->assertCount(0, $bundle_info->getBundleInfo('node'), 'The bundle information service has 0 bundles for the Node entity type.');
}
}

View File

@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\BrowserTestBase;
/**
* Ensures that node types translation work correctly.
*
* Note that the child site is installed in French; therefore, when making
* assertions on translated text it is important to provide a langcode. This
* ensures the asserts pass regardless of the Drupal version.
*
* @group node
*/
class NodeTypeTranslationTest extends BrowserTestBase {
use StringTranslationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'block',
'config_translation',
'field_ui',
'node',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The default language code to use in this test.
*
* @var array
*/
protected $defaultLangcode = 'fr';
/**
* Languages to enable.
*
* @var array
*/
protected $additionalLangcodes = ['es'];
/**
* Administrator user for tests.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$admin_permissions = [
'administer content types',
'bypass node access',
'administer node fields',
'administer languages',
'administer site configuration',
'administer themes',
'translate configuration',
];
// Create and log in user.
$this->adminUser = $this->drupalCreateUser($admin_permissions);
// Add languages.
foreach ($this->additionalLangcodes as $langcode) {
ConfigurableLanguage::createFromLangcode($langcode)->save();
}
}
/**
* {@inheritdoc}
*
* Install Drupal in a language other than English for this test. This is not
* needed to test the node type translation itself but acts as a regression
* test.
*
* @see https://www.drupal.org/node/2584603
*/
protected function installParameters() {
$parameters = parent::installParameters();
$parameters['parameters']['langcode'] = $this->defaultLangcode;
// Create an empty po file so we don't attempt to download one from
// localize.drupal.org. It does not need to match the version exactly as the
// multi-lingual system will fallback.
\Drupal::service('file_system')->mkdir($this->publicFilesDirectory . '/translations', NULL, TRUE);
file_put_contents($this->publicFilesDirectory . "/translations/drupal-8.0.0.{$this->defaultLangcode}.po", '');
return $parameters;
}
/**
* Tests the node type translation.
*/
public function testNodeTypeTranslation(): void {
$type = $this->randomMachineName(16);
$name = $this->randomString();
$this->drupalLogin($this->adminUser);
$this->drupalCreateContentType(['type' => $type, 'name' => $name]);
// Translate the node type name.
$langcode = $this->additionalLangcodes[0];
$translated_name = $langcode . '-' . $name;
$edit = [
"translation[config_names][node.type.$type][name]" => $translated_name,
];
// Edit the title label to avoid having an exception when we save the
// translation.
$this->drupalGet("admin/structure/types/manage/{$type}/translate/{$langcode}/add");
$this->submitForm($edit, 'Save translation');
// Check the name is translated without admin theme for editing.
$this->drupalGet('admin/appearance');
$this->submitForm(['use_admin_theme' => '0'], 'Save configuration');
$this->drupalGet("$langcode/node/add/$type");
// This is a Spanish page, so ensure the text asserted is translated in
// Spanish and not French by adding the langcode option.
$this->assertSession()->responseContains($this->t('Create @name', ['@name' => $translated_name], ['langcode' => $langcode]));
// Check the name is translated with admin theme for editing.
$this->drupalGet('admin/appearance');
$this->submitForm(['use_admin_theme' => '1'], 'Save configuration');
$this->drupalGet("$langcode/node/add/$type");
// This is a Spanish page, so ensure the text asserted is translated in
// Spanish and not French by adding the langcode option.
$this->assertSession()->responseContains($this->t('Create @name', ['@name' => $translated_name], ['langcode' => $langcode]));
}
/**
* Tests the node type title label translation.
*/
public function testNodeTypeTitleLabelTranslation(): void {
$type = $this->randomMachineName(16);
$name = $this->randomString();
$this->drupalLogin($this->adminUser);
$this->drupalCreateContentType(['type' => $type, 'name' => $name]);
$langcode = $this->additionalLangcodes[0];
// Edit the title label for it to be displayed on the translation form.
$this->drupalGet("admin/structure/types/manage/{$type}");
$this->submitForm(['title_label' => 'Edited title'], 'Save');
// Assert that the title label is displayed on the translation form with the
// right value.
$this->drupalGet("admin/structure/types/manage/$type/translate/$langcode/add");
$this->assertSession()->pageTextContains('Edited title');
// Translate the title label.
$this->submitForm(["translation[config_names][core.base_field_override.node.$type.title][label]" => 'Translated title'], 'Save translation');
// Assert that the right title label is displayed on the node add form. The
// translations are created in this test; therefore, the assertions do not
// use t(). If t() were used then the correct langcodes would need to be
// provided.
$this->drupalGet("node/add/$type");
$this->assertSession()->pageTextContains('Edited title');
$this->drupalGet("$langcode/node/add/$type");
$this->assertSession()->pageTextContains('Translated title');
// Add an email field.
$this->drupalGet("admin/structure/types/manage/{$type}/fields/add-field");
$this->clickLink('Email');
$this->submitForm([], 'Continue');
$this->submitForm([
'label' => 'Email',
'field_name' => 'email',
], 'Continue');
$this->submitForm([], 'Update settings');
$this->submitForm([], 'Save');
$type = $this->randomMachineName(16);
$name = $this->randomString();
$this->drupalCreateContentType(['type' => $type, 'name' => $name]);
// Set tabs.
$this->drupalPlaceBlock('local_tasks_block', ['primary' => TRUE]);
// Change default language.
$this->drupalGet('admin/config/regional/language');
$this->submitForm(['site_default_language' => 'es'], 'Save configuration');
// Try re-using the email field.
$this->drupalGet("es/admin/structure/types/manage/$type/fields/reuse");
$this->submitForm([], 'Re-use');
$this->assertSession()->statusCodeEquals(200);
$this->drupalGet("es/admin/structure/types/manage/$type/fields/node.$type.field_email/translate");
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains("The configuration objects have different language codes so they cannot be translated");
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\language\Entity\ConfigurableLanguage;
/**
* Tests the node language extra field display.
*
* @group node
*/
class NodeViewLanguageTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'datetime', 'language'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests the language extra field display.
*/
public function testViewLanguage(): void {
// Add Spanish language.
ConfigurableLanguage::createFromLangcode('es')->save();
// Set language field visible.
\Drupal::service('entity_display.repository')
->getViewDisplay('node', 'page', 'full')
->setComponent('langcode')
->save();
// Create a node in Spanish.
$node = $this->drupalCreateNode(['langcode' => 'es']);
$this->drupalGet($node->toUrl());
$this->assertSession()->pageTextContains('Spanish');
}
}

View File

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
/**
* Tests the node/{node} page.
*
* @group node
* @see \Drupal\node\Controller\NodeController
*/
class NodeViewTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests the html head links.
*/
public function testHtmlHeadLinks(): void {
$node = $this->drupalCreateNode();
$this->drupalGet($node->toUrl());
$element = $this->assertSession()->elementExists('css', 'link[rel="canonical"]');
$this->assertEquals($node->toUrl()->setAbsolute()->toString(), $element->getAttribute('href'));
$element = $this->assertSession()->elementExists('css', 'link[rel="shortlink"]');
$this->assertEquals($node->toUrl('canonical', ['alias' => TRUE])->setAbsolute()->toString(), $element->getAttribute('href'));
}
/**
* Tests the Link header.
*/
public function testLinkHeader(): void {
$node = $this->drupalCreateNode();
$this->drupalGet($node->toUrl());
$this->assertArrayNotHasKey('Link', $this->getSession()->getResponseHeaders());
}
/**
* Tests that we store and retrieve multi-byte UTF-8 characters correctly.
*/
public function testMultiByteUtf8(): void {
$title = '🐝';
// To ensure that the title has multi-byte characters, we compare the byte
// length to the character length.
$this->assertLessThan(strlen($title), mb_strlen($title, 'utf-8'));
$node = $this->drupalCreateNode(['title' => $title]);
$this->drupalGet($node->toUrl());
// Verify that the passed title was returned.
$this->assertSession()->elementTextEquals('xpath', '//h1/span', $title);
}
}

View File

@ -0,0 +1,552 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Url;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\node\Entity\NodeType;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
use Drupal\Tests\TestFileCreationTrait;
use Drupal\user\RoleInterface;
/**
* Tests the node entity preview functionality.
*
* @group node
*/
class PagePreviewTest extends NodeTestBase {
use EntityReferenceFieldCreationTrait;
use CommentTestTrait;
use TestFileCreationTrait {
getTestFiles as drupalGetTestFiles;
}
/**
* Enable the comment, node and taxonomy modules to test on the preview.
*
* @var array
*/
protected static $modules = [
'node',
'taxonomy',
'comment',
'image',
'file',
'text',
'node_test',
'menu_ui',
];
/**
* The theme to install as the default for testing.
*
* @var string
*/
protected $defaultTheme = 'starterkit_theme';
/**
* The name of the created field.
*
* @var string
*/
protected $fieldName;
/**
* A term.
*
* @var \Drupal\taxonomy\Entity\Term
*/
protected Term $term;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->addDefaultCommentField('node', 'page');
$web_user = $this->drupalCreateUser([
'edit own page content',
'create page content',
'administer menu',
]);
$this->drupalLogin($web_user);
// Add a vocabulary so we can test different view modes.
$vocabulary = Vocabulary::create([
'name' => $this->randomMachineName(),
'description' => $this->randomMachineName(),
'vid' => $this->randomMachineName(),
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
'help' => '',
]);
$vocabulary->save();
// Add a term to the vocabulary.
$term = Term::create([
'name' => $this->randomMachineName(),
'description' => $this->randomMachineName(),
'vid' => $vocabulary->id(),
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
]);
$term->save();
$this->term = $term;
// Create an image field.
FieldStorageConfig::create([
'field_name' => 'field_image',
'entity_type' => 'node',
'type' => 'image',
'settings' => [],
'cardinality' => FieldStorageConfig::CARDINALITY_UNLIMITED,
])->save();
$field_config = FieldConfig::create([
'field_name' => 'field_image',
'label' => 'Images',
'entity_type' => 'node',
'bundle' => 'page',
'required' => FALSE,
'settings' => [],
]);
$field_config->save();
// Create a field.
$this->fieldName = $this->randomMachineName();
$handler_settings = [
'target_bundles' => [
$vocabulary->id() => $vocabulary->id(),
],
'auto_create' => TRUE,
];
$this->createEntityReferenceField('node', 'page', $this->fieldName, 'Tags', 'taxonomy_term', 'default', $handler_settings, FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
$display_repository->getFormDisplay('node', 'page')
->setComponent($this->fieldName, [
'type' => 'entity_reference_autocomplete_tags',
])
->save();
// Show on default display and teaser.
$display_repository->getViewDisplay('node', 'page')
->setComponent($this->fieldName, [
'type' => 'entity_reference_label',
])
->save();
$display_repository->getViewDisplay('node', 'page', 'teaser')
->setComponent($this->fieldName, [
'type' => 'entity_reference_label',
])
->save();
$display_repository->getFormDisplay('node', 'page')
->setComponent('field_image', [
'type' => 'image_image',
'settings' => [],
])
->save();
$display_repository->getViewDisplay('node', 'page')
->setComponent('field_image')
->save();
// Create a multi-value text field.
$field_storage = FieldStorageConfig::create([
'field_name' => 'field_test_multi',
'entity_type' => 'node',
'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
'type' => 'text',
'settings' => [
'max_length' => 50,
],
]);
$field_storage->save();
FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'page',
])->save();
$display_repository->getFormDisplay('node', 'page')
->setComponent('field_test_multi', [
'type' => 'text_textfield',
])
->save();
$display_repository->getViewDisplay('node', 'page')
->setComponent('field_test_multi', [
'type' => 'string',
])
->save();
}
/**
* Checks the node preview functionality.
*/
public function testPagePreview(): void {
$title_key = 'title[0][value]';
$body_key = 'body[0][value]';
$term_key = $this->fieldName . '[target_id]';
// Fill in node creation form and preview node.
$edit = [];
$edit[$title_key] = '<em>' . $this->randomMachineName(8) . '</em>';
$edit[$body_key] = $this->randomMachineName(16);
$edit[$term_key] = $this->term->getName();
// Upload an image.
$test_image = current($this->drupalGetTestFiles('image', 39325));
$edit['files[field_image_0][]'] = \Drupal::service('file_system')->realpath($test_image->uri);
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Upload');
// Add an alt tag and preview the node.
$this->submitForm(['field_image[0][alt]' => 'Picture of llamas'], 'Preview');
// Check that the preview is displaying the title, body and term.
$expected_title = $edit[$title_key] . ' | Drupal';
$this->assertSession()->titleEquals($expected_title);
$this->assertSession()->assertEscaped($edit[$title_key]);
$this->assertSession()->pageTextContains($edit[$body_key]);
$this->assertSession()->pageTextContains($edit[$term_key]);
$this->assertSession()->linkExists('Back to content editing');
// Check that we see the class of the node type on the body element.
$this->assertSession()->elementExists('xpath', "//body[contains(@class, 'page-node-type-page')]");
// Get the UUID.
$url = parse_url($this->getUrl());
$paths = explode('/', $url['path']);
array_pop($paths);
$uuid = array_pop($paths);
// Switch view mode. We'll remove the body from the teaser view mode.
\Drupal::service('entity_display.repository')
->getViewDisplay('node', 'page', 'teaser')
->removeComponent('body')
->save();
$view_mode_edit = ['view_mode' => 'teaser'];
$this->drupalGet('node/preview/' . $uuid . '/full');
$this->submitForm($view_mode_edit, 'Switch');
$this->assertSession()->responseContains('view-mode-teaser');
$this->assertSession()->pageTextNotContains($edit[$body_key]);
// Check that the title, body and term fields are displayed with the
// values after going back to the content edit page.
$this->clickLink('Back to content editing');
$this->assertSession()->fieldValueEquals($title_key, $edit[$title_key]);
$this->assertSession()->fieldValueEquals($body_key, $edit[$body_key]);
$this->assertSession()->fieldValueEquals($term_key, $edit[$term_key]);
$this->assertSession()->fieldValueEquals('field_image[0][alt]', 'Picture of llamas');
$this->getSession()->getPage()->pressButton('Add another item');
$this->assertSession()->fieldExists('field_test_multi[0][value]');
$this->assertSession()->fieldExists('field_test_multi[1][value]');
// Return to page preview to check everything is as expected.
$this->submitForm([], 'Preview');
$this->assertSession()->titleEquals($expected_title);
$this->assertSession()->assertEscaped($edit[$title_key]);
$this->assertSession()->pageTextContains($edit[$body_key]);
$this->assertSession()->pageTextContains($edit[$term_key]);
$this->assertSession()->linkExists('Back to content editing');
// Assert the content is kept when reloading the page.
$this->drupalGet('node/add/page', ['query' => ['uuid' => $uuid]]);
$this->assertSession()->fieldValueEquals($title_key, $edit[$title_key]);
$this->assertSession()->fieldValueEquals($body_key, $edit[$body_key]);
$this->assertSession()->fieldValueEquals($term_key, $edit[$term_key]);
// Save the node - this is a new POST, so we need to upload the image.
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Upload');
$this->submitForm(['field_image[0][alt]' => 'Picture of llamas'], 'Save');
$node = $this->drupalGetNodeByTitle($edit[$title_key]);
// Check the term was displayed on the saved node.
$this->drupalGet('node/' . $node->id());
$this->assertSession()->pageTextContains($edit[$term_key]);
// Check the term appears again on the edit form.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->fieldValueEquals($term_key, $edit[$term_key] . ' (' . $this->term->id() . ')');
// Check with two new terms on the edit form, additionally to the existing
// one.
$edit = [];
$new_term1 = $this->randomMachineName(8);
$new_term2 = $this->randomMachineName(8);
$edit[$term_key] = $this->term->getName() . ', ' . $new_term1 . ', ' . $new_term2;
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Preview');
$this->assertSession()->responseContains('>' . $new_term1 . '<');
$this->assertSession()->responseContains('>' . $new_term2 . '<');
// The first term should be displayed as link, the others not.
$this->assertSession()->linkExists($this->term->getName());
$this->assertSession()->linkNotExists($new_term1);
$this->assertSession()->linkNotExists($new_term2);
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
// Check with one more new term, keeping old terms, removing the existing
// one.
$edit = [];
$new_term3 = $this->randomMachineName(8);
$edit[$term_key] = $new_term1 . ', ' . $new_term3 . ', ' . $new_term2;
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Preview');
$this->assertSession()->responseContains('>' . $new_term1 . '<');
$this->assertSession()->responseContains('>' . $new_term2 . '<');
$this->assertSession()->responseContains('>' . $new_term3 . '<');
$this->assertSession()->pageTextNotContains($this->term->getName());
$this->assertSession()->linkExists($new_term1);
$this->assertSession()->linkExists($new_term2);
$this->assertSession()->linkNotExists($new_term3);
// Check that editing an existing node after it has been previewed and not
// saved doesn't remember the previous changes.
$edit = [
$title_key => $this->randomMachineName(8),
];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Preview');
$this->assertSession()->pageTextContains($edit[$title_key]);
$this->clickLink('Back to content editing');
$this->assertSession()->fieldValueEquals($title_key, $edit[$title_key]);
// Navigate away from the node without saving.
$this->drupalGet('<front>');
// Go back to the edit form, the title should have its initial value.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->fieldValueEquals($title_key, $node->label());
// Check with required preview.
$node_type = NodeType::load('page');
$node_type->setPreviewMode(DRUPAL_REQUIRED);
$node_type->save();
$this->drupalGet('node/add/page');
$this->assertSession()->responseNotContains('edit-submit');
$this->drupalGet('node/add/page');
$this->submitForm([$title_key => 'Preview'], 'Preview');
$this->clickLink('Back to content editing');
$this->assertSession()->responseContains('edit-submit');
// Check that destination is remembered when clicking on preview. When going
// back to the edit form and clicking save, we should go back to the
// original destination, if set.
$destination = 'node';
$this->drupalGet($node->toUrl('edit-form'), ['query' => ['destination' => $destination]]);
$this->submitForm([], 'Preview');
$parameters = ['node_preview' => $node->uuid(), 'view_mode_id' => 'full'];
$options = ['absolute' => TRUE, 'query' => ['destination' => $destination]];
$this->assertSession()->addressEquals(Url::fromRoute('entity.node.preview', $parameters, $options));
$this->submitForm(['view_mode' => 'teaser'], 'Switch');
$this->clickLink('Back to content editing');
$this->submitForm([], 'Save');
$this->assertSession()->addressEquals($destination);
// Check that preview page works as expected without a destination set.
$this->drupalGet($node->toUrl('edit-form'));
$this->submitForm([], 'Preview');
$parameters = ['node_preview' => $node->uuid(), 'view_mode_id' => 'full'];
$this->assertSession()->addressEquals(Url::fromRoute('entity.node.preview', $parameters));
$this->submitForm(['view_mode' => 'teaser'], 'Switch');
$this->clickLink('Back to content editing');
$this->submitForm([], 'Save');
$this->assertSession()->addressEquals($node->toUrl());
$this->assertSession()->statusCodeEquals(200);
/** @var \Drupal\Core\File\FileSystemInterface $file_system */
$file_system = \Drupal::service('file_system');
// Assert multiple items can be added and are not lost when previewing.
$test_image_1 = current($this->drupalGetTestFiles('image', 39325));
$edit_image_1['files[field_image_0][]'] = $file_system->realpath($test_image_1->uri);
$test_image_2 = current($this->drupalGetTestFiles('image', 39325));
$edit_image_2['files[field_image_1][]'] = $file_system->realpath($test_image_2->uri);
$edit['field_image[0][alt]'] = 'Alt 1';
$this->drupalGet('node/add/page');
$this->submitForm($edit_image_1, 'Upload');
$this->submitForm($edit, 'Preview');
$this->clickLink('Back to content editing');
$this->assertSession()->fieldExists('files[field_image_1][]');
$this->submitForm($edit_image_2, 'Upload');
$this->assertSession()->fieldNotExists('files[field_image_1][]');
$title = 'node_test_title';
$example_text_1 = 'example_text_preview_1';
$example_text_2 = 'example_text_preview_2';
$example_text_3 = 'example_text_preview_3';
$this->drupalGet('node/add/page');
$edit = [
'title[0][value]' => $title,
'field_test_multi[0][value]' => $example_text_1,
];
$this->assertSession()->pageTextContains('Storage is not set');
$this->submitForm($edit, 'Preview');
$this->clickLink('Back to content editing');
$this->assertSession()->pageTextContains('Storage is set');
$this->assertSession()->fieldExists('field_test_multi[0][value]');
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains('Basic page ' . $title . ' has been created.');
$node = $this->drupalGetNodeByTitle($title);
$this->drupalGet('node/' . $node->id() . '/edit');
$this->getSession()->getPage()->pressButton('Add another item');
$this->getSession()->getPage()->pressButton('Add another item');
$edit = [
'field_test_multi[1][value]' => $example_text_2,
'field_test_multi[2][value]' => $example_text_3,
];
$this->submitForm($edit, 'Preview');
$this->clickLink('Back to content editing');
$this->submitForm($edit, 'Preview');
$this->clickLink('Back to content editing');
$this->assertSession()->fieldValueEquals('field_test_multi[0][value]', $example_text_1);
$this->assertSession()->fieldValueEquals('field_test_multi[1][value]', $example_text_2);
$this->assertSession()->fieldValueEquals('field_test_multi[2][value]', $example_text_3);
// Now save the node and make sure all values got saved.
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains($example_text_1);
$this->assertSession()->pageTextContains($example_text_2);
$this->assertSession()->pageTextContains($example_text_3);
// Edit again, change the menu_ui settings and click on preview.
$this->drupalGet('node/' . $node->id() . '/edit');
$edit = [
'menu[enabled]' => TRUE,
'menu[title]' => 'Changed title',
];
$this->submitForm($edit, 'Preview');
$this->clickLink('Back to content editing');
$this->assertSession()->checkboxChecked('edit-menu-enabled');
$this->assertSession()->fieldValueEquals('menu[title]', 'Changed title');
// Save, change the title while saving and make sure that it is correctly
// saved.
$edit = [
'menu[enabled]' => TRUE,
'menu[title]' => 'Second title change',
];
$this->submitForm($edit, 'Save');
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->fieldValueEquals('menu[title]', 'Second title change');
}
/**
* Checks the node preview functionality, when using revisions.
*/
public function testPagePreviewWithRevisions(): void {
$title_key = 'title[0][value]';
$body_key = 'body[0][value]';
$term_key = $this->fieldName . '[target_id]';
// Force revision on "Basic page" content.
$node_type = NodeType::load('page');
$node_type->setNewRevision(TRUE);
$node_type->save();
// Fill in node creation form and preview node.
$edit = [];
$edit[$title_key] = $this->randomMachineName(8);
$edit[$body_key] = $this->randomMachineName(16);
$edit[$term_key] = $this->term->id();
$edit['revision_log[0][value]'] = $this->randomString(32);
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Preview');
// Check that the preview is displaying the title, body and term.
$this->assertSession()->titleEquals($edit[$title_key] . ' | Drupal');
$this->assertSession()->pageTextContains($edit[$title_key]);
$this->assertSession()->pageTextContains($edit[$body_key]);
$this->assertSession()->pageTextContains($edit[$term_key]);
// Check that the title and body fields are displayed with the correct
// values after going back to the content edit page.
$this->clickLink('Back to content editing');
$this->assertSession()->fieldValueEquals($title_key, $edit[$title_key]);
$this->assertSession()->fieldValueEquals($body_key, $edit[$body_key]);
$this->assertSession()->fieldValueEquals($term_key, $edit[$term_key]);
// Check that the revision log field has the correct value.
$this->assertSession()->fieldValueEquals('revision_log[0][value]', $edit['revision_log[0][value]']);
// Save the node after coming back from the preview page so we can create a
// pending revision for it.
$this->submitForm([], 'Save');
$node = $this->drupalGetNodeByTitle($edit[$title_key]);
// Check that previewing a pending revision of a node works. This can not be
// accomplished through the UI so we have to use API calls.
// @todo Change this test to use the UI when we will be able to create
// pending revisions in core.
// @see https://www.drupal.org/node/2725533
$node->setNewRevision(TRUE);
$node->isDefaultRevision(FALSE);
/** @var \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver */
$controller_resolver = \Drupal::service('controller_resolver');
$node_preview_controller = $controller_resolver->getControllerFromDefinition('\Drupal\node\Controller\NodePreviewController::view');
$node_preview_controller($node, 'full');
}
/**
* Checks the node preview accessible for simultaneous node editing.
*/
public function testSimultaneousPreview(): void {
$title_key = 'title[0][value]';
$node = $this->drupalCreateNode([]);
$edit = [$title_key => 'New page title'];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Preview');
$this->assertSession()->pageTextContains($edit[$title_key]);
$user2 = $this->drupalCreateUser(['edit any page content']);
$this->drupalLogin($user2);
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->fieldValueEquals($title_key, $node->label());
$edit2 = [$title_key => 'Another page title'];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit2, 'Preview');
$this->assertSession()->addressEquals(Url::fromRoute('entity.node.preview', ['node_preview' => $node->uuid(), 'view_mode_id' => 'full']));
$this->assertSession()->pageTextContains($edit2[$title_key]);
}
/**
* Tests node preview with dynamic_page_cache and anonymous users.
*/
public function testPagePreviewCache(): void {
\Drupal::service('module_installer')->uninstall(['node_test']);
$this->drupalLogout();
$title_key = 'title[0][value]';
user_role_grant_permissions(RoleInterface::ANONYMOUS_ID, ['create page content', 'access content']);
$edit = [
$title_key => $this->randomMachineName(8),
];
$this->drupalGet('/node/add/page');
$this->submitForm($edit, 'Preview');
$this->assertSession()->pageTextContains($edit[$title_key]);
$this->clickLink('Back to content editing');
$edit = [
$title_key => $this->randomMachineName(8),
];
$this->submitForm($edit, 'Preview');
$this->assertSession()->pageTextContains($edit[$title_key]);
}
}

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\node\Entity\Node;
/**
* Create a node and test edit permissions.
*
* @group node
*/
class PageViewTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests editing a node by users with various access permissions.
*/
public function testPageView(): void {
// Create a node to view.
$node = $this->drupalCreateNode();
$this->assertNotEmpty(Node::load($node->id()), 'Node created.');
// Try to edit with anonymous user.
$this->drupalGet("node/" . $node->id() . "/edit");
$this->assertSession()->statusCodeEquals(403);
// Create a user without permission to edit node.
$web_user = $this->drupalCreateUser(['access content']);
$this->drupalLogin($web_user);
// Attempt to access edit page.
$this->drupalGet("node/" . $node->id() . "/edit");
$this->assertSession()->statusCodeEquals(403);
// Create user with permission to edit node.
$web_user = $this->drupalCreateUser(['bypass node access']);
$this->drupalLogin($web_user);
// Attempt to access edit page.
$this->drupalGet("node/" . $node->id() . "/edit");
$this->assertSession()->statusCodeEquals(200);
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group rest
*/
class NodeJsonAnonTest extends NodeResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
*/
class NodeJsonBasicAuthTest extends NodeResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method): void {
parent::setUpAuthorization($method);
$this->grantPermissionsToTestedRole(['view camelids revisions']);
}
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
$entity = parent::getExpectedNormalizedEntity();
$entity['revision_log'] = [];
return $entity;
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group rest
*/
class NodeJsonCookieTest extends NodeResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@ -0,0 +1,298 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Rest;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
use Drupal\user\Entity\User;
use GuzzleHttp\RequestOptions;
use PHPUnit\Framework\Attributes\Before;
/**
* Resource test base for the node entity.
*/
abstract class NodeResourceTestBase extends EntityResourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['content_translation', 'node', 'path'];
/**
* {@inheritdoc}
*/
protected static $entityTypeId = 'node';
/**
* {@inheritdoc}
*/
protected static $patchProtectedFieldNames = [
'revision_timestamp' => NULL,
'revision_uid' => 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'.",
];
/**
* @var \Drupal\node\NodeInterface
*/
protected $entity;
/**
* Marks some tests as skipped because XML cannot be deserialized.
*/
#[Before]
public function nodeResourceTestBaseSkipTests(): void {
if (static::$format === 'xml' && $this->name() === 'testPatchPath') {
$this->markTestSkipped('Deserialization of the XML format is not supported.');
}
}
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
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 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(static::$auth ? $this->account->id() : 0)
->setPublished()
->setCreatedTime(123456789)
->setChangedTime(123456789)
->setRevisionCreationTime(123456789)
->set('path', '/llama')
->save();
return $node;
}
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
$author = User::load($this->entity->getOwnerId());
return [
'nid' => [
['value' => 1],
],
'uuid' => [
['value' => $this->entity->uuid()],
],
'vid' => [
['value' => 1],
],
'langcode' => [
[
'value' => 'en',
],
],
'type' => [
[
'target_id' => 'camelids',
'target_type' => 'node_type',
'target_uuid' => NodeType::load('camelids')->uuid(),
],
],
'title' => [
[
'value' => 'Llama',
],
],
'status' => [
[
'value' => TRUE,
],
],
'created' => [
[
'value' => (new \DateTime())->setTimestamp(123456789)->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
'format' => \DateTime::RFC3339,
],
],
'changed' => [
[
'value' => (new \DateTime())->setTimestamp($this->entity->getChangedTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
'format' => \DateTime::RFC3339,
],
],
'promote' => [
[
'value' => TRUE,
],
],
'sticky' => [
[
'value' => FALSE,
],
],
'revision_timestamp' => [
[
'value' => (new \DateTime())->setTimestamp(123456789)->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
'format' => \DateTime::RFC3339,
],
],
'revision_translation_affected' => [
[
'value' => TRUE,
],
],
'default_langcode' => [
[
'value' => TRUE,
],
],
'uid' => [
[
'target_id' => (int) $author->id(),
'target_type' => 'user',
'target_uuid' => $author->uuid(),
'url' => base_path() . 'user/' . $author->id(),
],
],
'revision_uid' => [
[
'target_id' => (int) $author->id(),
'target_type' => 'user',
'target_uuid' => $author->uuid(),
'url' => base_path() . 'user/' . $author->id(),
],
],
'path' => [
[
'alias' => '/llama',
'pid' => 1,
'langcode' => 'en',
],
],
];
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPostEntity() {
return [
'type' => [
[
'target_id' => 'camelids',
],
],
'title' => [
[
'value' => 'Drama llama',
],
],
];
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
if ($method === 'GET' || $method == 'PATCH' || $method == 'DELETE' || $method == 'POST') {
return "The 'access content' permission is required.";
}
return parent::getExpectedUnauthorizedAccessMessage($method);
}
/**
* {@inheritdoc}
*/
protected function getExpectedCacheContexts() {
return [
'languages:language_interface',
'url.site',
'user.permissions',
];
}
/**
* 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\rest\Functional\EntityResource\Term\TermResourceTestBase::testPatchPath()
*/
public function testPatchPath(): void {
$this->initAuthentication();
$this->provisionEntityResource();
$this->setUpAuthorization('GET');
$this->setUpAuthorization('PATCH');
$url = $this->getEntityResourceUrl()->setOption('query', ['_format' => static::$format]);
// GET node's current normalization.
$response = $this->request('GET', $url, $this->getAuthenticationRequestOptions('GET'));
$normalization = $this->serializer->decode((string) $response->getBody(), static::$format);
// Change node's path alias.
$normalization['path'][0]['alias'] .= 's-rule-the-world';
// Create node PATCH request.
$request_options = [];
$request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType;
$request_options = array_merge_recursive($request_options, $this->getAuthenticationRequestOptions('PATCH'));
$request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
// PATCH request: 403 when creating URL aliases unauthorized. Before
// asserting the 403 response, assert that the stored path alias remains
// unchanged.
$response = $this->request('PATCH', $url, $request_options);
$this->assertSame('/llama', $this->entityStorage->loadUnchanged($this->entity->id())->get('path')->alias);
$this->assertResourceErrorResponse(403, "Access denied on updating field 'path'. " . static::$patchProtectedFieldNames['path'], $response);
// Make sure the role save below properly invalidates cache tags.
$this->refreshVariables();
// Grant permission to create URL aliases.
$this->grantPermissionsToTestedRole(['create url aliases']);
// Repeat PATCH request: 200.
$response = $this->request('PATCH', $url, $request_options);
$this->assertResourceResponse(200, FALSE, $response);
$updated_normalization = $this->serializer->decode((string) $response->getBody(), static::$format);
$this->assertSame($normalization['path'], $updated_normalization['path']);
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group rest
*/
class NodeTypeJsonAnonTest extends NodeTypeResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
*/
class NodeTypeJsonBasicAuthTest extends NodeTypeResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group rest
*/
class NodeTypeJsonCookieTest extends NodeTypeResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Rest;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\rest\Functional\EntityResource\ConfigEntityResourceTestBase;
/**
* Resource test base for NodeType entity.
*/
abstract class NodeTypeResourceTestBase extends ConfigEntityResourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node'];
/**
* {@inheritdoc}
*/
protected static $entityTypeId = 'node_type';
/**
* The NodeType entity.
*
* @var \Drupal\node\NodeTypeInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
$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 getExpectedNormalizedEntity() {
return [
'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,
'type' => 'camelids',
'uuid' => $this->entity->uuid(),
];
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPostEntity() {
// @todo Update in https://www.drupal.org/node/2300677.
return [];
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
return "The 'access content' permission is required.";
}
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class NodeTypeXmlAnonTest extends NodeTypeResourceTestBase {
use AnonResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class NodeTypeXmlBasicAuthTest extends NodeTypeResourceTestBase {
use BasicAuthResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class NodeTypeXmlCookieTest extends NodeTypeResourceTestBase {
use CookieResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class NodeXmlAnonTest extends NodeResourceTestBase {
use AnonResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class NodeXmlBasicAuthTest extends NodeResourceTestBase {
use BasicAuthResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class NodeXmlCookieTest extends NodeResourceTestBase {
use CookieResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@ -0,0 +1,199 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Views;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\node\Traits\NodeAccessTrait;
/**
* Tests if entity access is respected on a node bulk operations form.
*
* @group node
* @see \Drupal\node\Plugin\views\field\BulkForm
* @see \Drupal\node\Tests\NodeTestBase
* @see \Drupal\node\Tests\NodeAccessBaseTableTest
* @see \Drupal\node\Tests\Views\BulkFormTest
*/
class BulkFormAccessTest extends NodeTestBase {
use NodeAccessTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['node_test_views', 'node_access_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_node_bulk_form'];
/**
* The node access control handler.
*
* @var \Drupal\Core\Entity\EntityAccessControlHandlerInterface
*/
protected $accessHandler;
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE, $modules = ['node_test_views']): void {
parent::setUp($import_test_views, $modules);
// Create Article node type.
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
$this->accessHandler = \Drupal::entityTypeManager()->getAccessControlHandler('node');
$this->addPrivateField(NodeType::load('article'));
// After enabling a node access module, the access table has to be rebuild.
node_access_rebuild();
// Enable the private node feature of the node_access_test module.
\Drupal::state()->set('node_access_test.private', TRUE);
}
/**
* Tests if nodes that may not be edited, can not be edited in bulk.
*/
public function testNodeEditAccess(): void {
// Create an account who will be the author of a private node.
$author = $this->drupalCreateUser();
// Create a private node (author may view, edit and delete, others may not).
$node = $this->drupalCreateNode([
'type' => 'article',
'private' => [
['value' => TRUE],
],
'uid' => $author->id(),
]);
// Create an account that may view the private node, but not edit it.
$account = $this->drupalCreateUser(['node test view']);
$this->drupalLogin($account);
// Ensure the node is published.
$this->assertTrue($node->isPublished(), 'Node is initially published.');
// Ensure that the node can not be edited.
$this->assertFalse($this->accessHandler->access($node, 'update', $account), 'The node may not be edited.');
// Test editing the node using the bulk form.
$edit = [
'node_bulk_form[0]' => TRUE,
'action' => 'node_unpublish_action',
];
$this->drupalGet('test-node-bulk-form');
$this->submitForm($edit, 'Apply to selected items');
$this->assertSession()->pageTextContains("No access to execute Unpublish content on the Content {$node->label()}.");
// Re-load the node and check the status.
$node = Node::load($node->id());
$this->assertTrue($node->isPublished(), 'The node is still published.');
// Create an account that may view the private node, but can update the
// status.
$account = $this->drupalCreateUser(['administer nodes', 'node test view']);
$this->drupalLogin($account);
// Ensure the node is published.
$this->assertTrue($node->isPublished(), 'Node is initially published.');
// Ensure that the private node can not be edited.
$this->assertFalse($node->access('update', $account), 'The node may not be edited.');
$this->assertTrue($node->status->access('edit', $account), 'The node status can be edited.');
// Test editing the node using the bulk form.
$edit = [
'node_bulk_form[0]' => TRUE,
'action' => 'node_unpublish_action',
];
$this->drupalGet('test-node-bulk-form');
$this->submitForm($edit, 'Apply to selected items');
// Test that the action message isn't shown.
$this->assertSession()->pageTextNotContains("Unpublish content was applied to 1 item.");
// Re-load the node and check the status.
$node = Node::load($node->id());
$this->assertTrue($node->isPublished(), 'The node is still published.');
// Try to delete the node and check that we are not redirected to the
// conformation form but stay on the content view.
$this->assertNotEmpty($this->cssSelect('#views-form-test-node-bulk-form-page-1'));
$edit = [
'node_bulk_form[0]' => TRUE,
'action' => 'node_delete_action',
];
$this->drupalGet('test-node-bulk-form');
$this->submitForm($edit, 'Apply to selected items');
// Test that the action message isn't shown.
$this->assertSession()->pageTextContains("No access to execute Delete content on the Content {$node->label()}.");
$this->assertNotEmpty($this->cssSelect('#views-form-test-node-bulk-form-page-1'));
}
/**
* Tests if nodes that may not be deleted, can not be deleted in bulk.
*/
public function testNodeDeleteAccess(): void {
// Create an account who will be the author of a private node.
$author = $this->drupalCreateUser();
// Create a private node (author may view, edit and delete, others may not).
$private_node = $this->drupalCreateNode([
'type' => 'article',
'private' => [
['value' => TRUE],
],
'uid' => $author->id(),
]);
// Create an account that may view the private node, but not delete it.
$account = $this->drupalCreateUser([
'access content',
'administer nodes',
'delete own article content',
'node test view',
]);
// Create a node that may be deleted too, to ensure the delete confirmation
// page is shown later. In node_access_test.module, nodes may only be
// deleted by the author.
$own_node = $this->drupalCreateNode([
'type' => 'article',
'private' => [
['value' => TRUE],
],
'uid' => $account->id(),
]);
$this->drupalLogin($account);
// Ensure that the private node can not be deleted.
$this->assertFalse($this->accessHandler->access($private_node, 'delete', $account), 'The private node may not be deleted.');
// Ensure that the public node may be deleted.
$this->assertTrue($this->accessHandler->access($own_node, 'delete', $account), 'The own node may be deleted.');
// Try to delete the node using the bulk form.
$edit = [
'node_bulk_form[0]' => TRUE,
'node_bulk_form[1]' => TRUE,
'action' => 'node_delete_action',
];
$this->drupalGet('test-node-bulk-form');
$this->submitForm($edit, 'Apply to selected items');
$this->submitForm([], 'Delete');
// Ensure the private node still exists.
$private_node = Node::load($private_node->id());
$this->assertNotNull($private_node, 'The private node has not been deleted.');
// Ensure the own node is deleted.
$own_node = Node::load($own_node->id());
$this->assertNull($own_node, 'The own node is deleted.');
}
}

View File

@ -0,0 +1,301 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Views;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\views\Views;
/**
* Tests a node bulk form.
*
* @group node
* @see \Drupal\node\Plugin\views\field\BulkForm
*/
class BulkFormTest extends NodeTestBase {
/**
* Modules to be enabled.
*
* @var array
*/
protected static $modules = ['node_test_views', 'language'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_node_bulk_form'];
/**
* The test nodes.
*
* @var \Drupal\node\NodeInterface[]
*/
protected $nodes;
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE, $modules = ['node_test_views']): void {
parent::setUp($import_test_views, $modules);
ConfigurableLanguage::createFromLangcode('en-gb')->save();
ConfigurableLanguage::createFromLangcode('it')->save();
// Create some test nodes.
$this->nodes = [];
$langcodes = ['en', 'en-gb', 'it'];
for ($i = 1; $i <= 5; $i++) {
$langcode = $langcodes[($i - 1) % 3];
$values = [
'title' => $this->randomMachineName() . ' [' . $i . ':' . $langcode . ']',
'langcode' => $langcode,
'promote' => FALSE,
];
$node = $this->drupalCreateNode($values);
$this->nodes[] = $node;
}
// Create translations for all languages for some nodes.
for ($i = 0; $i < 2; $i++) {
$node = $this->nodes[$i];
foreach ($langcodes as $langcode) {
if (!$node->hasTranslation($langcode)) {
$title = $this->randomMachineName() . ' [' . $node->id() . ':' . $langcode . ']';
$node->addTranslation($langcode, ['title' => $title, 'promote' => FALSE]);
}
}
$node->save();
}
// Create a node with only one translation.
$node = $this->nodes[2];
$langcode = 'en';
$title = $this->randomMachineName() . ' [' . $node->id() . ':' . $langcode . ']';
$node->addTranslation($langcode, ['title' => $title]);
$node->save();
// Check that all created translations are selected by the test view.
$view = Views::getView('test_node_bulk_form');
$view->execute();
$this->assertCount(10, $view->result, 'All created translations are selected.');
// Check the operations are accessible to the logged in user.
$this->drupalLogin($this->drupalCreateUser([
'administer nodes',
'access content overview',
'bypass node access',
]));
$this->drupalGet('test-node-bulk-form');
$elements = $this->assertSession()->selectExists('edit-action')->findAll('css', 'option');
$this->assertCount(9, $elements, 'All node operations are found.');
}
/**
* Tests the node bulk form.
*/
public function testBulkForm(): void {
// Unpublish a node using the bulk form.
$node = reset($this->nodes);
$this->assertTrue($node->isPublished(), 'Node is initially published');
$this->assertTrue($node->getTranslation('en-gb')->isPublished(), 'Node translation is published');
$this->assertTrue($node->getTranslation('it')->isPublished(), 'Node translation is published');
$edit = [
'node_bulk_form[0]' => TRUE,
'action' => 'node_unpublish_action',
];
$this->submitForm($edit, 'Apply to selected items');
$node = $this->loadNode($node->id());
$this->assertFalse($node->isPublished(), 'Node has been unpublished');
$this->assertTrue($node->getTranslation('en-gb')->isPublished(), 'Node translation has not been unpublished');
$this->assertTrue($node->getTranslation('it')->isPublished(), 'Node translation has not been unpublished');
// Publish action.
$edit = [
'node_bulk_form[0]' => TRUE,
'action' => 'node_publish_action',
];
$this->submitForm($edit, 'Apply to selected items');
$node = $this->loadNode($node->id());
$this->assertTrue($node->isPublished(), 'Node has been published again');
// Make sticky action.
$this->assertFalse($node->isSticky(), 'Node is not sticky');
$this->assertFalse($node->getTranslation('en-gb')->isSticky(), 'Node translation is not sticky');
$this->assertFalse($node->getTranslation('it')->isSticky(), 'Node translation is not sticky');
$edit = [
'node_bulk_form[0]' => TRUE,
'action' => 'node_make_sticky_action',
];
$this->submitForm($edit, 'Apply to selected items');
$node = $this->loadNode($node->id());
$this->assertTrue($node->isSticky(), 'Node has been made sticky');
$this->assertFalse($node->getTranslation('en-gb')->isSticky(), 'Node translation has not been made sticky');
$this->assertFalse($node->getTranslation('it')->isSticky(), 'Node translation has not been made sticky');
// Make unsticky action.
$edit = [
'node_bulk_form[0]' => TRUE,
'action' => 'node_make_unsticky_action',
];
$this->submitForm($edit, 'Apply to selected items');
$node = $this->loadNode($node->id());
$this->assertFalse($node->isSticky(), 'Node is not sticky anymore');
// Promote to front page.
$this->assertFalse($node->isPromoted(), 'Node is not promoted to the front page');
$this->assertFalse($node->getTranslation('en-gb')->isPromoted(), 'Node translation is not promoted to the front page');
$this->assertFalse($node->getTranslation('it')->isPromoted(), 'Node translation is not promoted to the front page');
$edit = [
'node_bulk_form[0]' => TRUE,
'action' => 'node_promote_action',
];
$this->submitForm($edit, 'Apply to selected items');
$node = $this->loadNode($node->id());
$this->assertTrue($node->isPromoted(), 'Node has been promoted to the front page');
$this->assertFalse($node->getTranslation('en-gb')->isPromoted(), 'Node translation has not been promoted to the front page');
$this->assertFalse($node->getTranslation('it')->isPromoted(), 'Node translation has not been promoted to the front page');
// Demote from front page.
$edit = [
'node_bulk_form[0]' => TRUE,
'action' => 'node_unpromote_action',
];
$this->submitForm($edit, 'Apply to selected items');
$node = $this->loadNode($node->id());
$this->assertFalse($node->isPromoted(), 'Node has been demoted');
// Select a bunch of translated and untranslated nodes and check that
// operations are always applied to individual translations.
$edit = [
// Original and all translations.
// Node 1, English, original.
'node_bulk_form[0]' => TRUE,
// Node 1, British English.
'node_bulk_form[1]' => TRUE,
// Node 1, Italian.
'node_bulk_form[2]' => TRUE,
// Original and only one translation.
// Node 2, English.
'node_bulk_form[3]' => TRUE,
// Node 2, British English, original.
'node_bulk_form[4]' => TRUE,
// Node 2, Italian.
'node_bulk_form[5]' => FALSE,
// Only a single translation.
// Node 3, English.
'node_bulk_form[6]' => TRUE,
// Node 3, Italian, original.
'node_bulk_form[7]' => FALSE,
// Only a single untranslated node.
// Node 4, English, untranslated.
'node_bulk_form[8]' => TRUE,
// Node 5, British English, untranslated.
'node_bulk_form[9]' => FALSE,
'action' => 'node_unpublish_action',
];
$this->submitForm($edit, 'Apply to selected items');
$node = $this->loadNode(1);
$this->assertFalse($node->getTranslation('en')->isPublished(), '1: English translation has been unpublished');
$this->assertFalse($node->getTranslation('en-gb')->isPublished(), '1: British English translation has been unpublished');
$this->assertFalse($node->getTranslation('it')->isPublished(), '1: Italian translation has been unpublished');
$node = $this->loadNode(2);
$this->assertFalse($node->getTranslation('en')->isPublished(), '2: English translation has been unpublished');
$this->assertFalse($node->getTranslation('en-gb')->isPublished(), '2: British English translation has been unpublished');
$this->assertTrue($node->getTranslation('it')->isPublished(), '2: Italian translation has not been unpublished');
$node = $this->loadNode(3);
$this->assertFalse($node->getTranslation('en')->isPublished(), '3: English translation has been unpublished');
$this->assertTrue($node->getTranslation('it')->isPublished(), '3: Italian translation has not been unpublished');
$node = $this->loadNode(4);
$this->assertFalse($node->isPublished(), '4: Node has been unpublished');
$node = $this->loadNode(5);
$this->assertTrue($node->isPublished(), '5: Node has not been unpublished');
}
/**
* Tests multiple deletion.
*/
public function testBulkDeletion(): void {
// Select a bunch of translated and untranslated nodes and check that
// nodes and individual translations are properly deleted.
$edit = [
// Original and all translations.
// Node 1, English, original.
'node_bulk_form[0]' => TRUE,
// Node 1, British English.
'node_bulk_form[1]' => TRUE,
// Node 1, Italian.
'node_bulk_form[2]' => TRUE,
// Original and only one translation.
// Node 2, English.
'node_bulk_form[3]' => TRUE,
// Node 2, British English, original.
'node_bulk_form[4]' => TRUE,
// Node 2, Italian.
'node_bulk_form[5]' => FALSE,
// Only a single translation.
// Node 3, English.
'node_bulk_form[6]' => TRUE,
// Node 3, Italian, original.
'node_bulk_form[7]' => FALSE,
// Only a single untranslated node.
// Node 4, English, untranslated.
'node_bulk_form[8]' => TRUE,
// Node 5, British English, untranslated.
'node_bulk_form[9]' => FALSE,
'action' => 'node_delete_action',
];
$this->submitForm($edit, 'Apply to selected items');
$label = $this->loadNode(1)->label();
$this->assertSession()->pageTextContains("$label (Original translation) - The following content item translations will be deleted:");
$label = $this->loadNode(2)->label();
$this->assertSession()->pageTextContains("$label (Original translation) - The following content item translations will be deleted:");
$label = $this->loadNode(3)->getTranslation('en')->label();
$this->assertSession()->pageTextContains($label);
$this->assertSession()->pageTextNotContains("$label (Original translation) - The following content item translations will be deleted:");
$label = $this->loadNode(4)->label();
$this->assertSession()->pageTextContains($label);
$this->assertSession()->pageTextNotContains("$label (Original translation) - The following content item translations will be deleted:");
$this->submitForm([], 'Delete');
$node = $this->loadNode(1);
$this->assertNull($node, '1: Node has been deleted');
$node = $this->loadNode(2);
$this->assertNull($node, '2: Node has been deleted');
$node = $this->loadNode(3);
$result = count($node->getTranslationLanguages()) && $node->language()->getId() == 'it';
$this->assertTrue($result, '3: English translation has been deleted');
$node = $this->loadNode(4);
$this->assertNull($node, '4: Node has been deleted');
$node = $this->loadNode(5);
$this->assertNotEmpty($node, '5: Node has not been deleted');
$this->assertSession()->pageTextContains('Deleted 8 content items.');
}
/**
* Load the specified node from the storage.
*
* @param int $id
* The node identifier.
*
* @return \Drupal\node\NodeInterface
* The loaded node.
*/
protected function loadNode($id) {
/** @var \Drupal\node\NodeStorage $storage */
$storage = $this->container->get('entity_type.manager')->getStorage('node');
return $storage->load($id);
}
}

View File

@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Views;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\node\Traits\NodeAccessTrait;
/**
* Tests the node_access filter handler.
*
* @group node
* @see \Drupal\node\Plugin\views\filter\Access
*/
class FilterNodeAccessTest extends NodeTestBase {
use NodeAccessTrait;
/**
* An array of users.
*
* @var \Drupal\user\Entity\User[]
*/
protected $users;
/**
* {@inheritdoc}
*/
protected static $modules = ['node_access_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_filter_node_access'];
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE, $modules = ['node_test_views']): void {
parent::setUp($import_test_views, $modules);
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
$this->addPrivateField(NodeType::load('article'));
node_access_rebuild();
\Drupal::state()->set('node_access_test.private', TRUE);
$num_simple_users = 2;
$this->users = [];
for ($i = 0; $i < $num_simple_users; $i++) {
$this->users[$i] = $this->drupalCreateUser([
'access content',
'create article content',
]);
}
foreach ($this->users as $web_user) {
$this->drupalLogin($web_user);
foreach ([0 => 'Public', 1 => 'Private'] as $is_private => $type) {
$settings = [
'body' => [
[
'value' => $type . ' node',
'format' => filter_default_format(),
],
],
'title' => "$type Article created by " . $web_user->getAccountName(),
'type' => 'article',
'uid' => $web_user->id(),
'private' => (bool) $is_private,
];
$node = $this->drupalCreateNode($settings);
$this->assertEquals($is_private, (int) $node->private->value, 'The private status of the node was properly set in the node_access_test table.');
}
}
}
/**
* Tests the node access filter.
*/
public function testFilterNodeAccess(): void {
$this->drupalLogin($this->users[0]);
$this->drupalGet('test_filter_node_access');
// Test that the private node of the current user is shown.
$this->assertSession()->pageTextContains('Private Article created by ' . $this->users[0]->getAccountName());
// Test that the private node of the other use isn't shown.
$this->assertSession()->pageTextNotContains('Private Article created by ' . $this->users[1]->getAccountName());
// Test that both public nodes are shown.
$this->assertSession()->pageTextContains('Public Article created by ' . $this->users[0]->getAccountName());
$this->assertSession()->pageTextContains('Public Article created by ' . $this->users[1]->getAccountName());
// Switch users and test the other private node is shown.
$this->drupalLogin($this->users[1]);
$this->drupalGet('test_filter_node_access');
// Test that the private node of the current user is shown.
$this->assertSession()->pageTextContains('Private Article created by ' . $this->users[1]->getAccountName());
// Test that the private node of the other use isn't shown.
$this->assertSession()->pageTextNotContains('Private Article created by ' . $this->users[0]->getAccountName());
// Test that a user with administer nodes permission can't see all nodes.
$administer_nodes_user = $this->drupalCreateUser([
'access content',
'administer nodes',
]);
$this->drupalLogin($administer_nodes_user);
$this->drupalGet('test_filter_node_access');
$this->assertSession()->pageTextNotContains('Private Article created by ' . $this->users[0]->getAccountName());
$this->assertSession()->pageTextNotContains('Private Article created by ' . $this->users[1]->getAccountName());
$this->assertSession()->pageTextContains('Public Article created by ' . $this->users[0]->getAccountName());
$this->assertSession()->pageTextContains('Public Article created by ' . $this->users[1]->getAccountName());
// Test that a user with bypass node access can see all nodes.
$bypass_access_user = $this->drupalCreateUser([
'access content',
'bypass node access',
]);
$this->drupalLogin($bypass_access_user);
$this->drupalGet('test_filter_node_access');
$this->assertSession()->pageTextContains('Private Article created by ' . $this->users[0]->getAccountName());
$this->assertSession()->pageTextContains('Private Article created by ' . $this->users[1]->getAccountName());
$this->assertSession()->pageTextContains('Public Article created by ' . $this->users[0]->getAccountName());
$this->assertSession()->pageTextContains('Public Article created by ' . $this->users[1]->getAccountName());
}
}

View File

@ -0,0 +1,359 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Views;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Url;
use Drupal\node\Entity\Node;
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
use Drupal\Tests\views\Functional\ViewTestBase;
use Drupal\views\Tests\AssertViewsCacheTagsTrait;
use Drupal\views\ViewExecutable;
use Drupal\views\Views;
/**
* Tests the default frontpage provided by views.
*
* @group node
*/
class FrontPageTest extends ViewTestBase {
use AssertPageCacheContextsAndTagsTrait;
use AssertViewsCacheTagsTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The entity storage for nodes.
*
* @var \Drupal\node\NodeStorage
*/
protected $nodeStorage;
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'contextual'];
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE, $modules = []): void {
parent::setUp($import_test_views, $modules);
$this->nodeStorage = $this->container->get('entity_type.manager')
->getStorage('node');
}
/**
* Tests the frontpage.
*/
public function testFrontPage(): void {
$site_name = $this->randomMachineName();
$this->config('system.site')
->set('name', $site_name)
->save();
$view = Views::getView('frontpage');
// Tests
// \Drupal\node\Plugin\views\row\RssPluginBase::calculateDependencies().
$expected = [
'config' => [
'core.entity_view_mode.node.rss',
'core.entity_view_mode.node.teaser',
],
'module' => [
'node',
'user',
],
];
$this->assertSame($expected, $view->getDependencies());
$view->setDisplay('page_1');
$this->executeView($view);
$view->preview();
$this->assertEquals('Welcome!', $view->getTitle(), 'The welcome title is used for the empty view.');
$view->destroy();
// Create some nodes on the frontpage view. Add more than 10 nodes in order
// to enable paging.
$expected = [];
for ($i = 0; $i < 20; $i++) {
$values = [];
$values['type'] = 'article';
$values['title'] = $this->randomMachineName();
$values['promote'] = TRUE;
$values['status'] = TRUE;
// Test descending sort order.
$values['created'] = \Drupal::time()->getRequestTime() - $i;
// Test the sticky order.
if ($i == 5) {
$values['sticky'] = TRUE;
$node = $this->nodeStorage->create($values);
$node->save();
// Put the sticky on at the front.
array_unshift($expected, ['nid' => $node->id()]);
}
else {
$values['sticky'] = FALSE;
$node = $this->nodeStorage->create($values);
$node->save();
array_push($expected, ['nid' => $node->id()]);
}
}
// Create some nodes which aren't on the frontpage, either because they
// aren't promoted or because they aren't published.
$not_expected_nids = [];
$values = [];
$values['type'] = 'article';
$values['title'] = $this->randomMachineName();
$values['status'] = TRUE;
$values['promote'] = FALSE;
$node = $this->nodeStorage->create($values);
$node->save();
$not_expected_nids[] = $node->id();
$values['promote'] = TRUE;
$values['status'] = FALSE;
$values['title'] = $this->randomMachineName();
$node = $this->nodeStorage->create($values);
$node->save();
$not_expected_nids[] = $node->id();
$values['promote'] = TRUE;
$values['sticky'] = TRUE;
$values['status'] = FALSE;
$values['title'] = $this->randomMachineName();
$node = $this->nodeStorage->create($values);
$node->save();
$not_expected_nids[] = $node->id();
$column_map = ['nid' => 'nid'];
$view->setDisplay('page_1');
$this->executeView($view);
$this->assertIdenticalResultset($view, array_slice($expected, 0, 10), $column_map, 'Ensure that the right nodes are displayed on the frontpage.');
$this->assertNotInResultSet($view, $not_expected_nids, 'Ensure no unexpected node is in the result.');
$view->destroy();
$view->setDisplay('page_1');
$view->setCurrentPage(1);
$this->executeView($view);
$this->assertIdenticalResultset($view, array_slice($expected, 10, 10), $column_map, 'Ensure that the right nodes are displayed on second page of the frontpage.');
$this->assertNotInResultSet($view, $not_expected_nids, 'Ensure no unexpected node is in the result.');
$view->destroy();
}
/**
* Verifies that an amount of nids aren't in the result.
*
* @param \Drupal\views\ViewExecutable $view
* An executed View.
* @param array $not_expected_nids
* An array of nids which should not be part of the resultset.
* @param string $message
* (optional) A custom message to display with the assertion.
*
* @internal
*/
protected function assertNotInResultSet(ViewExecutable $view, array $not_expected_nids, string $message = ''): void {
$found_nids = array_filter($view->result, function ($row) use ($not_expected_nids) {
return in_array($row->nid, $not_expected_nids);
});
$this->assertEmpty($found_nids, $message);
}
/**
* Tests the cache tags when using the "none" cache plugin.
*/
public function testCacheTagsWithCachePluginNone(): void {
$this->enablePageCaching();
$this->doTestFrontPageViewCacheTags(FALSE);
}
/**
* Tests the cache tags when using the "tag" cache plugin.
*/
public function testCacheTagsWithCachePluginTag(): void {
$this->enablePageCaching();
$view = Views::getView('frontpage');
$view->setDisplay('page_1');
$view->display_handler->overrideOption('cache', [
'type' => 'tag',
]);
$view->save();
$this->doTestFrontPageViewCacheTags(TRUE);
}
/**
* Tests the cache tags when using the "time" cache plugin.
*/
public function testCacheTagsWithCachePluginTime(): void {
$this->enablePageCaching();
$view = Views::getView('frontpage');
$view->setDisplay('page_1');
$view->display_handler->overrideOption('cache', [
'type' => 'time',
'options' => [
'results_lifespan' => 3600,
'output_lifespan' => 3600,
],
]);
$view->save();
$this->doTestFrontPageViewCacheTags(TRUE);
}
/**
* Tests the cache tags on the front page.
*
* @param bool $do_assert_views_caches
* Whether to check Views' result & output caches.
*/
protected function doTestFrontPageViewCacheTags($do_assert_views_caches): void {
$view = Views::getView('frontpage');
$view->setDisplay('page_1');
$cache_contexts = [
// Cache contexts associated with the view.
'user.node_grants:view',
'languages:' . LanguageInterface::TYPE_INTERFACE,
// Cache contexts associated with the route's access checking.
'user.permissions',
// Default cache contexts of the renderer.
'theme',
'url.query_args',
// Attached feed.
'url.site',
];
$cache_context_tags = \Drupal::service('cache_contexts_manager')->convertTokensToKeys($cache_contexts)->getCacheTags();
// Test before there are any nodes.
$empty_node_listing_cache_tags = [
'config:views.view.frontpage',
'node_list',
];
$render_cache_tags = Cache::mergeTags($empty_node_listing_cache_tags, $cache_context_tags);
$this->assertViewsCacheTags(
$view,
$empty_node_listing_cache_tags,
$do_assert_views_caches,
$render_cache_tags
);
$expected_tags = Cache::mergeTags($empty_node_listing_cache_tags, $cache_context_tags);
$expected_tags = Cache::mergeTags($expected_tags, ['http_response', 'rendered', 'config:user.role.anonymous']);
$this->assertPageCacheContextsAndTags(
Url::fromRoute('view.frontpage.page_1'),
$cache_contexts,
$expected_tags
);
// Create some nodes on the frontpage view. Add more than 10 nodes in order
// to enable paging.
$this->drupalCreateContentType(['type' => 'article']);
for ($i = 0; $i < 15; $i++) {
$node = Node::create([
'body' => [
[
'value' => $this->randomMachineName(32),
'format' => filter_default_format(),
],
],
'type' => 'article',
'created' => $i,
'title' => $this->randomMachineName(8),
'nid' => $i + 1,
]);
$node->enforceIsNew(TRUE);
$node->save();
}
$cache_contexts = Cache::mergeContexts($cache_contexts, [
'timezone',
]);
// First page.
$first_page_result_cache_tags = [
'config:views.view.frontpage',
'node_list',
'node:6',
'node:7',
'node:8',
'node:9',
'node:10',
'node:11',
'node:12',
'node:13',
'node:14',
'node:15',
];
$cache_context_tags = \Drupal::service('cache_contexts_manager')->convertTokensToKeys($cache_contexts)->getCacheTags();
$first_page_output_cache_tags = Cache::mergeTags($first_page_result_cache_tags, $cache_context_tags);
$first_page_output_cache_tags = Cache::mergeTags($first_page_output_cache_tags, [
'config:filter.format.plain_text',
'node_view',
'user_view',
'user:0',
]);
$view->setDisplay('page_1');
$view->setCurrentPage(0);
$this->assertViewsCacheTags(
$view,
$first_page_result_cache_tags,
$do_assert_views_caches,
$first_page_output_cache_tags
);
$this->assertPageCacheContextsAndTags(
Url::fromRoute('view.frontpage.page_1'),
$cache_contexts,
Cache::mergeTags($first_page_output_cache_tags, ['http_response', 'rendered', 'config:user.role.anonymous'])
);
// Second page.
$this->assertPageCacheContextsAndTags(Url::fromRoute('view.frontpage.page_1', [], ['query' => ['page' => 1]]), $cache_contexts, [
// The cache tags for the listed nodes.
'node:1',
'node:2',
'node:3',
'node:4',
'node:5',
// The rest.
'config:filter.format.plain_text',
'config:views.view.frontpage',
'node_list',
'node_view',
'user_view',
'user:0',
'http_response',
'rendered',
// FinishResponseSubscriber adds this cache tag to responses that have the
// 'user.permissions' cache context for anonymous users.
'config:user.role.anonymous',
]);
// Let's update a node title on the first page and ensure that the page
// cache entry invalidates.
$node = Node::load(10);
$title = $node->getTitle() . 'a';
$node->setTitle($title);
$node->save();
$this->drupalGet(Url::fromRoute('view.frontpage.page_1'));
$this->assertSession()->pageTextContains($title);
}
}

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Views;
use Drupal\user\Entity\User;
/**
* Tests views contextual links on nodes.
*
* @group node
*/
class NodeContextualLinksTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['contextual'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests if the node page works if Contextual Links is disabled.
*
* All views have Contextual links enabled by default, even with the
* Contextual links module disabled. This tests if no calls are done to the
* Contextual links module by views when it is disabled.
*
* @see https://www.drupal.org/node/2379811
*/
public function testPageWithDisabledContextualModule(): void {
\Drupal::service('module_installer')->uninstall(['contextual']);
\Drupal::service('module_installer')->install(['views_ui']);
// Ensure that contextual links don't get called for admin users.
$admin_user = User::load(1);
$admin_user->setPassword('new_password');
$admin_user->passRaw = 'new_password';
$admin_user->save();
$this->drupalCreateContentType(['type' => 'page']);
$this->drupalCreateNode(['promote' => 1]);
$this->drupalLogin($admin_user);
$this->drupalGet('node');
}
}

View File

@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Views;
use Drupal\language\Entity\ConfigurableLanguage;
/**
* Tests node field filters with translations.
*
* @group node
*/
class NodeFieldFilterTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['language'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_field_filters'];
/**
* List of node titles by language.
*
* @var array
*/
public $nodeTitles = [];
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE, $modules = ['node_test_views']): void {
parent::setUp($import_test_views, $modules);
// Create Page content type.
if ($this->profile != 'standard') {
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
}
// Add two new languages.
ConfigurableLanguage::createFromLangcode('fr')->save();
ConfigurableLanguage::createFromLangcode('es')->save();
// Set up node titles.
$this->nodeTitles = [
'en' => 'Food in Paris',
'es' => 'Comida en Paris',
'fr' => 'Nourriture en Paris',
];
// Create node with translations.
$node = $this->drupalCreateNode(['title' => $this->nodeTitles['en'], 'langcode' => 'en', 'type' => 'page', 'body' => [['value' => $this->nodeTitles['en']]]]);
foreach (['es', 'fr'] as $langcode) {
$translation = $node->addTranslation($langcode, ['title' => $this->nodeTitles[$langcode]]);
$translation->body->value = $this->nodeTitles[$langcode];
}
$node->save();
}
/**
* Tests body and title filters.
*/
public function testFilters(): void {
// Test the title filter page, which filters for title contains 'Comida'.
// Should show just the Spanish translation, once.
$this->assertPageCounts('test-title-filter', ['es' => 1, 'fr' => 0, 'en' => 0], 'Comida title filter');
// Test the body filter page, which filters for body contains 'Comida'.
// Should show just the Spanish translation, once.
$this->assertPageCounts('test-body-filter', ['es' => 1, 'fr' => 0, 'en' => 0], 'Comida body filter');
// Test the title Paris filter page, which filters for title contains
// 'Paris'. Should show each translation once.
$this->assertPageCounts('test-title-paris', ['es' => 1, 'fr' => 1, 'en' => 1], 'Paris title filter');
// Test the body Paris filter page, which filters for body contains
// 'Paris'. Should show each translation once.
$this->assertPageCounts('test-body-paris', ['es' => 1, 'fr' => 1, 'en' => 1], 'Paris body filter');
}
/**
* Asserts that the given node translation counts are correct.
*
* @param string $path
* Path of the page to test.
* @param array $counts
* Array whose keys are languages, and values are the number of times
* that translation should be shown on the given page.
* @param string $message
* Message suffix to display.
*
* @internal
*/
protected function assertPageCounts(string $path, array $counts, string $message): void {
// Disable read more links.
\Drupal::service('entity_display.repository')
->getViewDisplay('node', 'page', 'teaser')
->removeComponent('links')
->save();
// Get the text of the page.
$this->drupalGet($path);
$text = $this->getTextContent();
// Check the counts. Note that the title and body are both shown on the
// page, and they are the same. So the title/body string should appear on
// the page twice as many times as the input count.
foreach ($counts as $langcode => $count) {
$this->assertEquals(2 * $count, substr_count($text, $this->nodeTitles[$langcode]), 'Translation ' . $langcode . ' has count ' . $count . ' with ' . $message);
}
}
}

View File

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Views;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
/**
* Tests replacement of Views tokens supplied by the Node module.
*
* @group node
* @see \Drupal\node\Tests\NodeTokenReplaceTest
*/
class NodeFieldTokensTest extends NodeTestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_node_tokens'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests token replacement for Views tokens supplied by the Node module.
*/
public function testViewsTokenReplacement(): void {
// Create the Article content type with a standard body field.
/** @var \Drupal\node\NodeTypeInterface $node_type */
$node_type = NodeType::create(['type' => 'article', 'name' => 'Article']);
$node_type->save();
node_add_body_field($node_type);
// Create a user and a node.
$account = $this->createUser();
$body = $this->randomMachineName(32);
$summary = $this->randomMachineName(16);
/** @var \Drupal\node\NodeInterface $node */
$node = Node::create([
'type' => 'article',
'uid' => $account->id(),
'title' => 'Testing Views tokens',
'body' => [['value' => $body, 'summary' => $summary, 'format' => 'plain_text']],
]);
$node->save();
$this->drupalGet('test_node_tokens');
// Body: {{ body }}<br />
$this->assertSession()->responseContains("Body: <p>$body</p>");
// Raw value: {{ body__value }}<br />
$this->assertSession()->responseContains("Raw value: $body");
// Raw summary: {{ body__summary }}<br />
$this->assertSession()->responseContains("Raw summary: $summary");
// Raw format: {{ body__format }}<br />
$this->assertSession()->responseContains("Raw format: plain_text");
}
}

View File

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Views;
/**
* Tests the node integration into views.
*
* @group node
*/
class NodeIntegrationTest extends NodeTestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_node_view'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests basic node view with a node type argument.
*/
public function testNodeViewTypeArgument(): void {
// Create two content types with three nodes each.
$types = [];
$all_nids = [];
for ($i = 0; $i < 2; $i++) {
$type = $this->drupalCreateContentType(['name' => '<em>' . $this->randomMachineName() . '</em>']);
$types[] = $type;
for ($j = 0; $j < 5; $j++) {
// Ensure the right order of the nodes.
$node = $this->drupalCreateNode(['type' => $type->id(), 'created' => \Drupal::time()->getRequestTime() - ($i * 5 + $j)]);
$nodes[$type->id()][$node->id()] = $node;
$all_nids[] = $node->id();
}
}
$this->drupalGet('test-node-view');
$this->assertSession()->statusCodeEquals(404);
$this->drupalGet('test-node-view/all');
$this->assertSession()->statusCodeEquals(200);
$this->assertNids($all_nids);
foreach ($types as $type) {
$this->drupalGet("test-node-view/{$type->id()}");
$this->assertSession()->assertEscaped($type->label());
$this->assertNids(array_keys($nodes[$type->id()]));
}
}
/**
* Ensures that a list of nodes appear on the page.
*
* @param array $expected_nids
* An array of node IDs.
*
* @internal
*/
protected function assertNids(array $expected_nids = []): void {
$result = $this->xpath('//span[@class="field-content"]');
$nids = [];
foreach ($result as $element) {
$nids[] = (int) $element->getText();
}
$this->assertEquals($expected_nids, $nids);
}
}

View File

@ -0,0 +1,307 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Views;
use Drupal\Core\Language\LanguageInterface;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\views\Plugin\views\PluginBase;
use Drupal\views\Tests\ViewTestData;
use Drupal\views\Views;
// cspell:ignore nodo nœud tercera
/**
* Tests node language fields, filters, and sorting.
*
* @group node
*/
class NodeLanguageTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['language', 'node_test_views'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_language'];
/**
* List of node titles by language.
*
* @var array
*/
public $nodeTitles = [];
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE, $modules = []): void {
parent::setUp(FALSE, $modules);
// Create Page content type.
if ($this->profile != 'standard') {
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
ViewTestData::createTestViews(static::class, ['node_test_views']);
}
// Add two new languages.
ConfigurableLanguage::createFromLangcode('fr')->save();
ConfigurableLanguage::createFromLangcode('es')->save();
// Make the body field translatable. The title is already translatable by
// definition.
$field_storage = FieldStorageConfig::loadByName('node', 'body');
$field_storage->setTranslatable(TRUE);
$field_storage->save();
// Set up node titles. They should not include the words "French",
// "English", or "Spanish", as there is a language field in the view
// that prints out those words.
$this->nodeTitles = [
LanguageInterface::LANGCODE_NOT_SPECIFIED => [
'First node und',
],
'es' => [
'Primero nodo es',
'Segundo nodo es',
'Tercera nodo es',
],
'en' => [
'First node en',
'Second node en',
],
'fr' => [
'Premier nœud fr',
],
];
// Create nodes with translations.
foreach ($this->nodeTitles['es'] as $index => $title) {
$node = $this->drupalCreateNode(['title' => $title, 'langcode' => 'es', 'type' => 'page', 'promote' => 1]);
foreach (['en', 'fr'] as $langcode) {
if (isset($this->nodeTitles[$langcode][$index])) {
$translation = $node->addTranslation($langcode, ['title' => $this->nodeTitles[$langcode][$index]]);
$translation->body->value = $this->randomMachineName(32);
}
}
$node->save();
}
// Create non-translatable nodes.
foreach ($this->nodeTitles[LanguageInterface::LANGCODE_NOT_SPECIFIED] as $index => $title) {
$node = $this->drupalCreateNode(['title' => $title, 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, 'type' => 'page', 'promote' => 1]);
$node->body->value = $this->randomMachineName(32);
$node->save();
}
$user = $this->drupalCreateUser([
'access content overview',
'access content',
]);
$this->drupalLogin($user);
}
/**
* Tests translation language filter, field, and sort.
*/
public function testLanguages(): void {
// Test the page with no arguments. It is filtered to Spanish and French.
// The page shows node titles and languages.
$this->drupalGet('test-language');
$message = 'French/Spanish page';
// Test that the correct nodes are shown.
foreach ($this->nodeTitles as $langcode => $list) {
foreach ($list as $title) {
if ($langcode == 'en') {
$this->assertSession()->pageTextNotContains($title);
}
else {
$this->assertSession()->pageTextContains($title);
}
}
}
// Test that the language field value is shown.
$this->assertSession()->pageTextNotContains('English');
$this->assertSession()->pageTextContains('French');
$this->assertSession()->pageTextContains('Spanish');
// Test page sorting, which is by language code, ascending. So the
// Spanish nodes should appear before the French nodes.
$page = $this->getTextContent();
$pos_es_max = 0;
$pos_fr_min = 10000;
foreach ($this->nodeTitles['es'] as $title) {
$pos_es_max = max($pos_es_max, strpos($page, $title));
}
foreach ($this->nodeTitles['fr'] as $title) {
$pos_fr_min = min($pos_fr_min, strpos($page, $title));
}
$this->assertLessThan($pos_fr_min, $pos_es_max, "The Spanish translation should appear before the French one on $message.");
// Test the argument -- filter to just Spanish.
$this->drupalGet('test-language/es');
// This time, test just the language field.
$message = 'Spanish argument page';
$this->assertSession()->pageTextNotContains('English');
$this->assertSession()->pageTextNotContains('French');
$this->assertSession()->pageTextContains('Spanish');
// Test the front page view filter. Only node titles in the current language
// should be displayed on the front page by default.
foreach ($this->nodeTitles as $langcode => $titles) {
// The frontpage view does not display content without a language.
if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) {
continue;
}
$this->drupalGet(($langcode == 'en' ? '' : "$langcode/") . 'node');
foreach ($titles as $title) {
$this->assertSession()->pageTextContains($title);
}
foreach ($this->nodeTitles as $control_langcode => $control_titles) {
if ($langcode != $control_langcode) {
foreach ($control_titles as $title) {
$this->assertSession()->pageTextNotContains($title);
}
}
}
}
// Test the node admin view filter. By default all translations should show.
$this->drupalGet('admin/content');
foreach ($this->nodeTitles as $titles) {
foreach ($titles as $title) {
$this->assertSession()->pageTextContains($title);
}
}
// When filtered, only the specific languages should show.
foreach ($this->nodeTitles as $langcode => $titles) {
$this->drupalGet('admin/content', ['query' => ['langcode' => $langcode]]);
foreach ($titles as $title) {
$this->assertSession()->pageTextContains($title);
}
foreach ($this->nodeTitles as $control_langcode => $control_titles) {
if ($langcode != $control_langcode) {
foreach ($control_titles as $title) {
$this->assertSession()->pageTextNotContains($title);
}
}
}
}
// Override the config for the front page view, so that the language
// filter is set to the site default language instead. This should just
// show the English nodes, no matter what the content language is.
$config = $this->config('views.view.frontpage');
$config->set('display.default.display_options.filters.langcode.value', [PluginBase::VIEWS_QUERY_LANGUAGE_SITE_DEFAULT => PluginBase::VIEWS_QUERY_LANGUAGE_SITE_DEFAULT]);
$config->save();
foreach ($this->nodeTitles as $langcode => $titles) {
if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) {
continue;
}
$this->drupalGet(($langcode == 'en' ? '' : "$langcode/") . 'node');
foreach ($this->nodeTitles as $control_langcode => $control_titles) {
foreach ($control_titles as $title) {
if ($control_langcode == 'en') {
$this->assertSession()->pageTextContains($title);
}
else {
$this->assertSession()->pageTextNotContains($title);
}
}
}
}
// Override the config so that the language filter is set to the UI
// language, and make that have a fixed value of 'es'.
//
// IMPORTANT: Make sure this part of the test is last -- it is changing
// language configuration!
$config->set('display.default.display_options.filters.langcode.value', ['***LANGUAGE_language_interface***' => '***LANGUAGE_language_interface***']);
$config->save();
$language_config = $this->config('language.types');
$language_config->set('negotiation.language_interface.enabled', ['language-selected' => 1]);
$language_config->save();
$language_config = $this->config('language.negotiation');
$language_config->set('selected_langcode', 'es');
$language_config->save();
// With a fixed language selected, there is no language-based URL.
$this->drupalGet('node');
foreach ($this->nodeTitles as $control_langcode => $control_titles) {
foreach ($control_titles as $title) {
if ($control_langcode == 'es') {
$this->assertSession()->pageTextContains($title);
}
else {
$this->assertSession()->pageTextNotContains($title);
}
}
}
}
/**
* Tests native name display in language field.
*/
public function testNativeLanguageField(): void {
$this->assertLanguageNames();
// Modify test view to display native language names and set translations.
$config = $this->config('views.view.test_language');
$config->set('display.default.display_options.fields.langcode.settings.native_language', TRUE);
$config->save();
\Drupal::languageManager()->getLanguageConfigOverride('fr', 'language.entity.fr')->set('label', 'Français')->save();
\Drupal::languageManager()->getLanguageConfigOverride('es', 'language.entity.es')->set('label', 'Español')->save();
$this->assertLanguageNames(TRUE);
// Modify test view to use the views built-in language field and test that.
\Drupal::state()->set('node_test_views.use_basic_handler', TRUE);
Views::viewsData()->clear();
$config = $this->config('views.view.test_language');
$config->set('display.default.display_options.fields.langcode.native_language', FALSE);
$config->clear('display.default.display_options.fields.langcode.settings');
$config->clear('display.default.display_options.fields.langcode.type');
$config->set('display.default.display_options.fields.langcode.plugin_id', 'language');
$config->save();
$this->assertLanguageNames();
$config->set('display.default.display_options.fields.langcode.native_language', TRUE)->save();
$this->assertLanguageNames(TRUE);
}
/**
* Asserts the presence of language names in their English or native forms.
*
* @param bool $native
* (optional) Whether to assert the language name in its native form.
*
* @internal
*/
protected function assertLanguageNames(bool $native = FALSE): void {
$this->drupalGet('test-language');
if ($native) {
$this->assertSession()->pageTextContains('Français');
$this->assertSession()->pageTextContains('Español');
$this->assertSession()->pageTextNotContains('French');
$this->assertSession()->pageTextNotContains('Spanish');
}
else {
$this->assertSession()->pageTextNotContains('Français');
$this->assertSession()->pageTextNotContains('Español');
$this->assertSession()->pageTextContains('French');
$this->assertSession()->pageTextContains('Spanish');
}
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Views;
use Drupal\Tests\views\Functional\ViewTestBase;
/**
* Base class for all node Views tests.
*/
abstract class NodeTestBase extends ViewTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node_test_views'];
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE, $modules = ['node_test_views']): void {
parent::setUp($import_test_views, $modules);
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Views;
/**
* Tests node_views_analyze().
*
* @group node
*/
class NodeViewsAnalyzeTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['views_ui', 'node_test_views'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_node_views_analyze'];
/**
* Tests the implementation of node_views_analyze().
*/
public function testNodeViewsAnalyze(): void {
// Create user with permission to view analyze message on views_ui.
$admin_user = $this->createUser(['administer views']);
$this->drupalLogin($admin_user);
// Access to views analyze page.
$this->drupalGet('admin/structure/views/nojs/analyze/test_node_views_analyze/page_1');
// Should return 200 with correct permission.
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->responseContains('has set node/% as path. This will not produce what you want. If you want to have multiple versions of the node view, use Layout Builder.');
}
}

View File

@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Views;
/**
* Tests the different revision link handlers.
*
* @group node
*
* @see \Drupal\node\Plugin\views\field\RevisionLink
* @see \Drupal\node\Plugin\views\field\RevisionLinkDelete
* @see \Drupal\node\Plugin\views\field\RevisionLinkRevert
*/
class RevisionLinkTest extends NodeTestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_node_revision_links'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests revision links.
*/
public function testRevisionLinks(): void {
// Create one user which can view/revert and delete and one which can only
// do one of them.
$this->drupalCreateContentType(['name' => 'page', 'type' => 'page']);
$account = $this->drupalCreateUser([
'revert all revisions',
'view all revisions',
'delete all revisions',
'edit any page content',
'delete any page content',
]);
$this->drupalLogin($account);
// Create two nodes, one without an additional revision and one with a
// revision.
$nodes = [
$this->drupalCreateNode(),
$this->drupalCreateNode(),
];
$first_revision = $nodes[1]->getRevisionId();
// Create revision of the node.
$nodes[1]->setNewRevision();
$nodes[1]->save();
$second_revision = $nodes[1]->getRevisionId();
$this->drupalGet('test-node-revision-links');
$this->assertSession()->statusCodeEquals(200);
// The first node revision should link to the node directly as you get an
// access denied if you link to the revision.
$url = $nodes[0]->toUrl()->toString();
$this->assertSession()->linkByHrefExists($url);
$this->assertSession()->linkByHrefNotExists($url . '/revisions/' . $nodes[0]->getRevisionId() . '/view');
$this->assertSession()->linkByHrefNotExists($url . '/revisions/' . $nodes[0]->getRevisionId() . '/delete');
$this->assertSession()->linkByHrefNotExists($url . '/revisions/' . $nodes[0]->getRevisionId() . '/revert');
// For the second node the current revision got set to the last revision, so
// the first one should also link to the node page itself.
$url = $nodes[1]->toUrl()->toString();
$this->assertSession()->linkByHrefExists($url);
$this->assertSession()->linkByHrefExists($url . '/revisions/' . $first_revision . '/view');
$this->assertSession()->linkByHrefExists($url . '/revisions/' . $first_revision . '/delete');
$this->assertSession()->linkByHrefExists($url . '/revisions/' . $first_revision . '/revert');
$this->assertSession()->linkByHrefNotExists($url . '/revisions/' . $second_revision . '/view');
$this->assertSession()->linkByHrefNotExists($url . '/revisions/' . $second_revision . '/delete');
$this->assertSession()->linkByHrefNotExists($url . '/revisions/' . $second_revision . '/revert');
$accounts = [
'view' => $this->drupalCreateUser(['view all revisions']),
'revert' => $this->drupalCreateUser([
'revert all revisions',
'edit any page content',
]),
'delete' => $this->drupalCreateUser([
'delete all revisions',
'delete any page content',
]),
];
$url = $nodes[1]->toUrl()->toString();
// Render the view with users which can only delete/revert revisions.
foreach ($accounts as $allowed_operation => $account) {
$this->drupalLogin($account);
$this->drupalGet('test-node-revision-links');
// Check expected links.
foreach (['revert', 'delete'] as $operation) {
if ($operation == $allowed_operation) {
$this->assertSession()->linkByHrefExists($url . '/revisions/' . $first_revision . '/' . $operation);
}
else {
$this->assertSession()->linkByHrefNotExists($url . '/revisions/' . $first_revision . '/' . $operation);
}
}
}
}
}

View File

@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Views;
use Drupal\node\Entity\NodeType;
use Drupal\node\NodeInterface;
use Drupal\Tests\node\Traits\NodeAccessTrait;
/**
* Tests the node.status_extra field handler.
*
* @group node
* @see \Drupal\node\Plugin\views\filter\Status
*/
class StatusExtraTest extends NodeTestBase {
use NodeAccessTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['node_test_views', 'content_moderation'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_status_extra'];
/**
* Tests the status extra filter.
*/
public function testStatusExtra(): void {
$node_author = $this->drupalCreateUser(['view own unpublished content']);
$node_author_not_unpublished = $this->drupalCreateUser();
$normal_user = $this->drupalCreateUser();
$privileged_user = $this->drupalCreateUser(['view any unpublished content']);
$admin_user = $this->drupalCreateUser(['bypass node access']);
// Create one published and one unpublished node by the admin.
$node_published = $this->drupalCreateNode(['uid' => $admin_user->id()]);
$node_unpublished = $this->drupalCreateNode(['uid' => $admin_user->id(), 'status' => NodeInterface::NOT_PUBLISHED]);
// Create one unpublished node by a certain author user.
$node_unpublished2 = $this->drupalCreateNode(['uid' => $node_author->id(), 'status' => NodeInterface::NOT_PUBLISHED]);
// Create one unpublished node by a user who does not have the `view own
// unpublished content` permission.
$node_unpublished3 = $this->drupalCreateNode(['uid' => $node_author_not_unpublished->id(), 'status' => NodeInterface::NOT_PUBLISHED]);
// The administrator should simply see all nodes.
$this->drupalLogin($admin_user);
$this->drupalGet('test_status_extra');
$this->assertSession()->pageTextContains($node_published->label());
$this->assertSession()->pageTextContains($node_unpublished->label());
$this->assertSession()->pageTextContains($node_unpublished2->label());
$this->assertSession()->pageTextContains($node_unpublished3->label());
// The privileged user should simply see all nodes.
$this->drupalLogin($privileged_user);
$this->drupalGet('test_status_extra');
$this->assertSession()->pageTextContains($node_published->label());
$this->assertSession()->pageTextContains($node_unpublished->label());
$this->assertSession()->pageTextContains($node_unpublished2->label());
$this->assertSession()->pageTextContains($node_unpublished3->label());
// The node author should see the published node and their own node.
$this->drupalLogin($node_author);
$this->drupalGet('test_status_extra');
$this->assertSession()->pageTextContains($node_published->label());
$this->assertSession()->pageTextNotContains($node_unpublished->label());
$this->assertSession()->pageTextContains($node_unpublished2->label());
$this->assertSession()->pageTextNotContains($node_unpublished3->label());
// The normal user should just see the published node.
$this->drupalLogin($normal_user);
$this->drupalGet('test_status_extra');
$this->assertSession()->pageTextContains($node_published->label());
$this->assertSession()->pageTextNotContains($node_unpublished->label());
$this->assertSession()->pageTextNotContains($node_unpublished2->label());
$this->assertSession()->pageTextNotContains($node_unpublished3->label());
// The author without the permission to see their own unpublished node
// should just see the published node.
$this->drupalLogin($node_author_not_unpublished);
$this->drupalGet('test_status_extra');
$this->assertSession()->pageTextContains($node_published->label());
$this->assertSession()->pageTextNotContains($node_unpublished->label());
$this->assertSession()->pageTextNotContains($node_unpublished2->label());
$this->assertSession()->pageTextNotContains($node_unpublished3->label());
\Drupal::service('module_installer')->install(['node_access_test']);
NodeType::create(['type' => 'page', 'name' => 'page'])->save();
$this->addPrivateField(NodeType::load('page'));
node_access_rebuild();
$node_published_private = $this->drupalCreateNode(['uid' => $admin_user->id(), 'private' => ['value' => 1]]);
$node_unpublished_private = $this->drupalCreateNode(['uid' => $admin_user->id(), 'status' => NodeInterface::NOT_PUBLISHED, 'private' => ['value' => 1]]);
// An unprivileged user must not see the published and unpublished content
// when access is granted via hook_node_access_grants().
$this->drupalLogin($this->drupalCreateUser());
$this->drupalGet('test_status_extra');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextNotContains($node_published_private->label());
$this->assertSession()->pageTextNotContains($node_unpublished_private->label());
// A privileged user must see the published and unpublished content
// when access is granted via hook_node_access_grants().
$this->drupalLogin($this->drupalCreateUser(values: [
'roles' => $this->drupalCreateRole([
'node test view',
]),
]));
$this->drupalGet('test_status_extra');
$this->assertSession()->pageTextContains($node_published_private->label());
$this->assertSession()->pageTextContains($node_unpublished_private->label());
}
}

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Views\Wizard;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\Tests\views\Functional\Wizard\WizardTestBase;
/**
* Tests node wizard and content type with hidden Taxonomy Term Reference field.
*
* @group Views
* @group node
*/
class HiddenTaxonomyTermReferenceFieldWizardTest extends WizardTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $modules = ['taxonomy'];
/**
* Tests content type with a hidden Taxonomy Term Reference field.
*/
public function testHiddenTaxonomyTermReferenceField(): void {
// Create Article node type.
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
// Create a taxonomy_term_reference field on the article Content Type. By
// not assigning a widget to that field we make sure it is hidden on the
// Form Display.
$field_name = $this->randomMachineName();
FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'node',
'type' => 'entity_reference',
'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
'settings' => [
'target_type' => 'taxonomy_term',
],
])->save();
FieldConfig::create([
'field_name' => $field_name,
'bundle' => 'article',
'entity_type' => 'node',
'settings' => [
'handler' => 'default',
],
])->save();
$this->drupalGet('admin/structure/views/add');
$this->assertSession()->statusCodeEquals(200);
}
}

View File

@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Views\Wizard;
use Drupal\Tests\views\Functional\Wizard\WizardTestBase;
use Drupal\views\Views;
/**
* Tests the wizard with node_revision as base table.
*
* @group node
* @see \Drupal\node\Plugin\views\wizard\NodeRevision
*/
class NodeRevisionWizardTest extends WizardTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests creating a node revision view.
*/
public function testViewAdd(): void {
$this->drupalCreateContentType(['type' => 'article']);
// Create two nodes with two revision.
$node_storage = \Drupal::entityTypeManager()->getStorage('node');
/** @var \Drupal\node\NodeInterface $node */
$node = $node_storage->create(['title' => $this->randomString(), 'type' => 'article', 'changed' => \Drupal::time()->getRequestTime() + 40]);
$node->save();
$node = $node->createDuplicate();
$node->setNewRevision();
$node->changed->value = \Drupal::time()->getRequestTime() + 20;
$node->save();
$node = $node_storage->create(['title' => $this->randomString(), 'type' => 'article', 'changed' => \Drupal::time()->getRequestTime() + 30]);
$node->save();
$node = $node->createDuplicate();
$node->setNewRevision();
$node->changed->value = \Drupal::time()->getRequestTime() + 10;
$node->save();
$this->drupalCreateContentType(['type' => 'not_article']);
$node = $node_storage->create(['title' => $this->randomString(), 'type' => 'not_article', 'changed' => \Drupal::time()->getRequestTime() + 80]);
$node->save();
$type = [
'show[wizard_key]' => 'node_revision',
];
$this->drupalGet('admin/structure/views/add');
$this->submitForm($type, 'Update "Show" choice');
$view = [];
$view['label'] = $this->randomMachineName(16);
$view['id'] = $this->randomMachineName(16);
$view['description'] = $this->randomMachineName(16);
$view['page[create]'] = FALSE;
$view['show[type]'] = 'article';
$view['show[sort]'] = 'changed:DESC';
$this->submitForm($view, 'Save and edit');
$view = Views::getView($view['id']);
$view->initHandlers();
$this->assertEquals(['node_field_revision' => TRUE, '#global' => TRUE, 'node_field_data' => TRUE], $view->getBaseTables());
// Check for the default filters.
$this->assertEquals('node_field_revision', $view->filter['status']->table);
$this->assertEquals('status', $view->filter['status']->field);
$this->assertEquals('1', $view->filter['status']->value);
$this->assertEquals('node_field_data', $view->filter['type']->table);
$this->executeView($view);
$this->assertIdenticalResultset($view, [['vid' => 1], ['vid' => 3], ['vid' => 2], ['vid' => 4]],
['vid' => 'vid']);
// Create a new view with no filter on type.
$type = [
'show[wizard_key]' => 'node_revision',
];
$this->drupalGet('admin/structure/views/add');
$this->submitForm($type, 'Update "Show" choice');
$view = [];
$view['label'] = $this->randomMachineName(16);
$view['id'] = $this->randomMachineName(16);
$view['description'] = $this->randomMachineName(16);
$view['page[create]'] = FALSE;
$view['show[type]'] = 'all';
$view['show[sort]'] = 'changed:DESC';
$this->submitForm($view, 'Save and edit');
$view = Views::getView($view['id']);
$view->initHandlers();
$this->assertEquals(['node_field_revision' => TRUE, '#global' => TRUE], $view->getBaseTables());
// Check for the default filters.
$this->assertEquals('node_field_revision', $view->filter['status']->table);
$this->assertEquals('status', $view->filter['status']->field);
$this->assertEquals('1', $view->filter['status']->value);
$this->assertArrayNotHasKey('type', $view->filter);
$this->executeView($view);
$this->assertIdenticalResultset($view, [['vid' => 5], ['vid' => 1], ['vid' => 3], ['vid' => 2], ['vid' => 4]],
['vid' => 'vid']);
}
}

View File

@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests that outlines of node meta values are displayed in summaries and tabs.
*
* @group node
*/
class CollapsedSummariesTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalCreateContentType([
'type' => 'page',
'name' => 'Basic page',
]);
$this->drupalCreateNode([
'title' => $this->randomMachineName(),
'type' => 'page',
]);
$this->drupalLogin($this->createUser(['edit any page content']));
}
/**
* Confirm that summaries are provided for node meta at all widths.
*/
public function testSummaries(): void {
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
// At a wider width, vertical tabs are used for the meta section of the node
// form.
$this->getSession()->resizeWindow(1200, 1200);
$this->drupalGet('node/1/edit');
$assert_session->waitForText("New revision");
$summary = $assert_session->waitForElement('css', '.vertical-tabs__menu-item-summary');
$this->assertNotNull($summary);
$this->assertTrue($summary->isVisible());
$this->assertEquals('New revision', $summary->getText());
$page->uncheckField('revision');
$assert_session->waitForText('No revision');
$this->assertEquals('No revision', $summary->getText());
// At a narrower width, details are used for the meta section of the node
// form.
$this->getSession()->resizeWindow(600, 1200);
$this->drupalGet('node/1/edit');
$summary = $assert_session->waitForElement('css', 'span.summary');
$this->assertNotNull($summary);
$this->assertTrue($summary->isVisible());
$page->uncheckField('revision');
$assert_session->waitForText('No revision');
$this->assertEquals('(No revision)', $summary->getText());
}
}

View File

@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\node\Entity\Node;
use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait;
/**
* Create a node with revisions and test contextual links.
*
* @group node
*/
class ContextualLinksTest extends WebDriverTestBase {
use ContextualLinkClickTrait;
/**
* An array of node revisions.
*
* @var \Drupal\node\NodeInterface[]
*/
protected $nodes;
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'contextual'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalCreateContentType([
'type' => 'page',
'name' => 'Basic page',
'display_submitted' => FALSE,
]);
// Create initial node.
$node = $this->drupalCreateNode();
$nodes = [];
// Get original node.
$nodes[] = clone $node;
// Create two revisions.
$revision_count = 2;
for ($i = 0; $i < $revision_count; $i++) {
// Create revision with a random title and body and update variables.
$node->title = $this->randomMachineName();
$node->body = [
'value' => $this->randomMachineName(32),
'format' => filter_default_format(),
];
$node->setNewRevision();
$node->save();
// Make sure we get revision information.
$node = Node::load($node->id());
$nodes[] = clone $node;
}
$this->nodes = $nodes;
$this->drupalLogin($this->createUser(
[
'view page revisions',
'revert page revisions',
'delete page revisions',
'edit any page content',
'delete any page content',
'access contextual links',
'administer content types',
]
));
}
/**
* Tests the contextual links on revisions.
*/
public function testRevisionContextualLinks(): void {
// Confirm that the "Edit" and "Delete" contextual links appear for the
// default revision.
$this->drupalGet('node/' . $this->nodes[0]->id());
$page = $this->getSession()->getPage();
$page->waitFor(10, function () use ($page) {
return $page->find('css', "main .contextual");
});
$this->toggleContextualTriggerVisibility('main');
$page->find('css', 'main .contextual button')->press();
$links = $page->findAll('css', "main .contextual-links li a");
$this->assertEquals('Edit', $links[0]->getText());
$this->assertEquals('Delete', $links[1]->getText());
// Confirm that "Edit" and "Delete" contextual links don't appear for
// non-default revision.
$this->drupalGet("node/" . $this->nodes[0]->id() . "/revisions/" . $this->nodes[1]->getRevisionId() . "/view");
$this->assertSession()->pageTextContains($this->nodes[1]->getTitle());
$page->waitFor(10, function () use ($page) {
return $page->find('css', "main .contextual");
});
$this->toggleContextualTriggerVisibility('main');
$contextual_button = $page->find('css', 'main .contextual button');
$this->assertEmpty(0, $contextual_button ?: '');
}
}

View File

@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\views\Views;
/**
* Tests JavaScript functionality specific to delete operations.
*
* @group node
*/
class NodeDeleteConfirmTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $modules = ['node'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalCreateContentType([
'name' => 'Article',
'type' => 'article',
]);
$admin_user = $this->drupalCreateUser([
'access content',
'access content overview',
'administer content types',
'edit any article content',
'delete any article content',
]);
$this->drupalLogin($admin_user);
}
/**
* Tests that the node delete operation opens in a modal.
*/
public function testNodeDelete(): void {
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
// Test the delete operation on an item in admin/content .
$node = $this->createNode([
'type' => 'article',
'title' => 'Delete article from content list',
]);
$node->save();
$this->drupalGet('admin/content');
$page->find('css', '.dropbutton-toggle button')->click();
$page->clickLink('Delete');
// Asserts a dialog opens with the expected text.
$this->assertEquals('Are you sure you want to delete the content item Delete article from content list?', $assert_session->waitForElement('css', '.ui-dialog-title')->getText());
$page->find('css', '.ui-dialog-buttonset')->pressButton('Delete');
$assert_session->pageTextContains('The Article Delete article from content list has been deleted.');
// Assert that the node is deleted in above operation.
$this->drupalGet('admin/content');
$assert_session->pageTextContains('There are no content items yet.');
$node = $this->createNode([
'type' => 'article',
'title' => 'Delete article from entity form',
]);
$node->save();
// Tests the delete modal on its entity form.
$this->drupalGet('node/2/edit');
$this->clickLink('Delete');
$this->assertEquals('Are you sure you want to delete the content item Delete article from entity form?', $assert_session->waitForElement('css', '.ui-dialog-title')->getText());
$page->find('css', '.ui-dialog-buttonset')->pressButton('Delete');
$assert_session->pageTextContains('The Article Delete article from entity form has been deleted.');
$node = $this->createNode([
'type' => 'article',
'title' => 'Delete article from views entity operations',
]);
$node->save();
\Drupal::service('module_installer')->install(['views']);
$view = Views::getView('content');
$view->storage->enable()->save();
\Drupal::service('router.builder')->rebuildIfNeeded();
$this->drupalGet('admin/content');
$page->find('css', '.dropbutton-toggle button')->click();
$page->clickLink('Delete');
// Asserts a dialog opens with the expected text.
$this->assertEquals('Are you sure you want to delete the content item Delete article from views entity operations?', $assert_session->waitForElement('css', '.ui-dialog-title')->getText());
$page->find('css', '.ui-dialog-buttonset')->pressButton('Delete');
$assert_session->pageTextContains('The Article Delete article from views entity operations has been deleted.');
$assert_session->pageTextContains('No content available.');
}
/**
* Tests that the node type delete operation opens in a modal.
*/
public function testNodeTypeDelete(): void {
$page = $this->getSession()->getPage();
// Delete node type using link on the content type list.
$this->drupalGet('admin/structure/types');
$this->assertSession()->waitForText('Article');
$page->find('css', '.dropbutton-toggle button')->click();
$this->clickLink('Delete');
$this->assertEquals('Are you sure you want to delete the content type Article?', $this->assertSession()->waitForElement('css', '.ui-dialog-title')->getText());
$page->find('css', '.ui-dialog-buttonset')->pressButton('Delete');
$this->assertSession()->pageTextContains('The content type Article has been deleted.');
$this->drupalCreateContentType([
'name' => 'Article',
'type' => 'article',
]);
// Delete node type using link on the edit content type form.
$this->drupalGet('admin/structure/types/manage/article');
$this->clickLink('Delete');
$this->assertEquals('Are you sure you want to delete the content type Article?', $this->assertSession()->waitForElement('css', '.ui-dialog-title')->getText());
$page->find('css', '.ui-dialog-buttonset')->pressButton('Delete');
$this->assertSession()->pageTextContains('The content type Article has been deleted.');
}
}

View File

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\FunctionalJavascript;
use Drupal\filter\Entity\FilterFormat;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests the JavaScript prevention of navigation away from node previews.
*
* @group node
*/
class NodePreviewLinkTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'filter'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$filtered_html_format = FilterFormat::create([
'format' => 'filtered_html',
'name' => 'Filtered HTML',
]);
$filtered_html_format->save();
$this->drupalCreateContentType(['type' => 'test']);
$user = $this->drupalCreateUser([
'access content',
'edit own test content',
'create test content',
$filtered_html_format->getPermissionName(),
]);
$this->drupalLogin($user);
}
/**
* Tests the behavior of clicking preview links.
*/
public function testPreviewLinks(): void {
$assertSession = $this->assertSession();
$this->drupalGet('node/add/test');
$this->submitForm([
'title[0][value]' => 'Test anchor link',
'body[0][value]' => '<a href="#foo">Anchor link</a>',
], 'Preview');
$this->clickLink('Anchor link');
$assertSession->pageTextNotContains('Leave preview?');
$this->drupalGet('node/add/test');
$this->submitForm([
'title[0][value]' => 'Test normal link',
'body[0][value]' => '<a href="/foo">Normal link</a>',
], 'Preview');
$this->clickLink('Normal link');
$assertSession->pageTextContains('Leave preview?');
$this->click('button:contains("Leave preview")');
$this->assertStringEndsWith('/foo', $this->getUrl());
$this->drupalGet('node/add/test');
$this->submitForm([
'title[0][value]' => 'Test child element link',
'body[0][value]' => '<a href="/foo" class="preview-child-element"><span>Child element link</span></a>',
], 'Preview');
$this->getSession()->getPage()->find('css', '.preview-child-element span')->click();
$assertSession->pageTextContains('Leave preview?');
$this->click('button:contains("Leave preview")');
$this->assertStringEndsWith('/foo', $this->getUrl());
}
}

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests the JavaScript updating of summaries on content type form.
*
* @group node
*/
class SettingSummariesContentTypeTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $modules = ['node'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$admin_user = $this->drupalCreateUser(['administer content types']);
$this->drupalLogin($admin_user);
$this->drupalCreateContentType(['type' => 'test']);
}
/**
* Tests a vertical tab 'Workflow' summary.
*/
public function testWorkflowSummary(): void {
$this->drupalGet('admin/structure/types/manage/test');
$page = $this->getSession()->getPage();
$page->find('css', 'a[href="#edit-workflow"]')->click();
$this->assertSession()->waitForElementVisible('css', '[name="options[status]"]');
$page->findField('options[status]')->uncheck();
$page->findField('options[sticky]')->check();
$page->findField('options[promote]')->check();
$page->findField('options[revision]')->check();
$summary = 'Not published, Promoted to front page, Sticky at top of lists, Create new revision';
$locator = '[href="#edit-workflow"] .vertical-tabs__menu-item-summary:contains("' . $summary . '")';
$this->assertNotEmpty($this->assertSession()->waitForElement('css', $locator));
}
}

View File

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Kernel\Config;
use Drupal\node\Entity\NodeType;
use Drupal\KernelTests\KernelTestBase;
/**
* Change content types during config create method invocation.
*
* @group node
*/
class NodeImportChangeTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'node',
'field',
'text',
'system',
'node_test_config',
'user',
];
/**
* Set the default field storage backend for fields created during tests.
*/
protected function setUp(): void {
parent::setUp();
// Set default storage backend.
$this->installConfig(['system', 'field', 'node_test_config']);
}
/**
* Tests importing an updated content type.
*/
public function testImportChange(): void {
$node_type_id = 'default';
$node_type_config_name = "node.type.$node_type_id";
// Simulate config data to import:
// - a modified version (modified label) of the node type config.
$active = $this->container->get('config.storage');
$sync = $this->container->get('config.storage.sync');
$this->copyConfig($active, $sync);
$node_type = $active->read($node_type_config_name);
$new_label = 'Test update import field';
$node_type['name'] = $new_label;
// Save as files in the sync directory.
$sync->write($node_type_config_name, $node_type);
// Import the content of the sync directory.
$this->configImporter()->import();
// Check that the updated config was correctly imported.
$node_type = NodeType::load($node_type_id);
$this->assertEquals($new_label, $node_type->label(), 'Node type name has been updated.');
}
}

View File

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Kernel\Config;
use Drupal\Core\Site\Settings;
use Drupal\field\Entity\FieldConfig;
use Drupal\node\Entity\NodeType;
use Drupal\KernelTests\KernelTestBase;
/**
* Create content types during config create method invocation.
*
* @group node
*/
class NodeImportCreateTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'field', 'text', 'system', 'user'];
/**
* Set the default field storage backend for fields created during tests.
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
// Set default storage backend.
$this->installConfig(['system', 'field']);
}
/**
* Tests creating a content type during default config import.
*/
public function testImportCreateDefault(): void {
$node_type_id = 'default';
// Check that the content type does not exist yet.
$this->assertNull(NodeType::load($node_type_id));
// Enable node_test_config module and check that the content type
// shipped in the module's default config is created.
$this->container->get('module_installer')->install(['node_test_config']);
$node_type = NodeType::load($node_type_id);
$this->assertNotEmpty($node_type, 'The default content type was created.');
}
/**
* Tests creating a content type during config import.
*/
public function testImportCreate(): void {
$node_type_id = 'import';
$node_type_config_name = "node.type.$node_type_id";
// Simulate config data to import.
$active = $this->container->get('config.storage');
$sync = $this->container->get('config.storage.sync');
$this->copyConfig($active, $sync);
// Manually add new node type.
$src_dir = __DIR__ . '/../../../modules/node_test_config/sync';
$target_dir = Settings::get('config_sync_directory');
$this->assertNotFalse(\Drupal::service('file_system')->copy("$src_dir/$node_type_config_name.yml", "$target_dir/$node_type_config_name.yml"));
// Import the content of the sync directory.
$this->configImporter()->import();
// Check that the content type was created.
$node_type = NodeType::load($node_type_id);
$this->assertNotEmpty($node_type, 'Import node type from sync was created.');
$this->assertNull(FieldConfig::loadByName('node', $node_type_id, 'body'));
}
}

View File

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Kernel;
use Drupal\Core\Config\Action\ConfigActionManager;
use Drupal\KernelTests\KernelTestBase;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
/**
* @group node
*/
class ConfigActionsTest extends KernelTestBase {
use ContentTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['field', 'node', 'system', 'text', 'user'];
/**
* The configuration action manager.
*/
private readonly ConfigActionManager $configActionManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('node');
$this->installConfig('node');
$this->configActionManager = $this->container->get('plugin.manager.config_action');
}
/**
* Tests the application of configuration actions on a node type.
*/
public function testConfigActions(): void {
$node_type = $this->createContentType();
$this->assertTrue($node_type->shouldCreateNewRevision());
$this->assertSame(DRUPAL_OPTIONAL, $node_type->getPreviewMode());
$this->assertTrue($node_type->displaySubmitted());
$this->configActionManager->applyAction(
'entity_method:node.type:setNewRevision',
$node_type->getConfigDependencyName(),
FALSE,
);
$this->configActionManager->applyAction(
'entity_method:node.type:setPreviewMode',
$node_type->getConfigDependencyName(),
DRUPAL_REQUIRED,
);
$this->configActionManager->applyAction(
'entity_method:node.type:setDisplaySubmitted',
$node_type->getConfigDependencyName(),
FALSE,
);
$node_type = NodeType::load($node_type->id());
$this->assertFalse($node_type->shouldCreateNewRevision());
$this->assertSame(DRUPAL_REQUIRED, $node_type->getPreviewMode());
$this->assertFalse($node_type->displaySubmitted());
}
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Kernel\Migrate;
use Drupal\Tests\migrate_drupal\Kernel\MigrateDrupalTestBase;
use Drupal\migrate_drupal\Tests\StubTestTrait;
use Drupal\node\Entity\NodeType;
/**
* Test stub creation for nodes.
*
* @group node
*/
class MigrateNodeStubTest extends MigrateDrupalTestBase {
use StubTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['node'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('node');
// Need at least one node type present.
NodeType::create([
'type' => 'testnodetype',
'name' => 'Test node type',
])->save();
}
/**
* Tests creation of node stubs.
*/
public function testStub(): void {
$this->performStubTest('node');
}
}

View File

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Kernel\Migrate\d6;
use Drupal\Core\Field\Entity\BaseFieldOverride;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
use Drupal\node\Entity\Node;
/**
* Test migrating node settings into the base_field_bundle_override config entity.
*
* @group migrate_drupal_6
*/
class MigrateNodeBundleSettingsTest extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['menu_ui'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(['node']);
$this->executeMigration('d6_node_type');
// Create a config entity that already exists.
BaseFieldOverride::create([
'field_name' => 'promote',
'entity_type' => 'node',
'bundle' => 'page',
'label' => 'Promoted to front page',
])->save();
$this->executeMigrations([
'd6_node_setting_promote',
'd6_node_setting_status',
'd6_node_setting_sticky',
]);
}
/**
* Tests Drupal 6 node type settings to Drupal 8 migration.
*/
public function testNodeBundleSettings(): void {
// Test settings on test_page bundle.
$node = Node::create(['type' => 'test_page']);
$this->assertSame(1, $node->status->value);
$this->assertSame(1, $node->promote->value);
$this->assertSame(1, $node->sticky->value);
// Test settings for test_story bundle.
$node = Node::create(['type' => 'test_story']);
$this->assertSame(1, $node->status->value);
$this->assertSame(1, $node->promote->value);
$this->assertSame(0, $node->sticky->value);
// Test settings for the test_event bundle.
$node = Node::create(['type' => 'test_event']);
$this->assertSame(0, $node->status->value);
$this->assertSame(0, $node->promote->value);
$this->assertSame(1, $node->sticky->value);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Kernel\Migrate\d6;
use Drupal\Tests\SchemaCheckTestTrait;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
/**
* Upgrade variables to node.settings.yml.
*
* @group migrate_drupal_6
*/
class MigrateNodeConfigsTest extends MigrateDrupal6TestBase {
use SchemaCheckTestTrait;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->executeMigration('d6_node_settings');
}
/**
* Tests Drupal 6 node settings to Drupal 8 migration.
*/
public function testNodeSettings(): void {
$config = $this->config('node.settings');
$this->assertFalse($config->get('use_admin_theme'));
$this->assertConfigSchema(\Drupal::service('config.typed'), 'node.settings', $config->get());
}
}

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Kernel\Migrate\d6;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
/**
* Test D6NodeDeriver.
*
* @group migrate_drupal_6
*/
class MigrateNodeDeriverTest extends MigrateDrupal6TestBase {
/**
* The migration plugin manager.
*
* @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface
*/
protected $pluginManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->pluginManager = $this->container->get('plugin.manager.migration');
}
/**
* Tests node translation migrations with translation disabled.
*/
public function testNoTranslations(): void {
// Without content_translation, there should be no translation migrations.
$migrations = $this->pluginManager->createInstances('d6_node_translation');
$this->assertSame([], $migrations,
"No node translation migrations without content_translation");
}
/**
* Tests node translation migrations with translation enabled.
*/
public function testTranslations(): void {
// With content_translation, there should be translation migrations for
// each content type.
$this->enableModules(['language', 'content_translation']);
$this->assertTrue($this->container->get('plugin.manager.migration')->hasDefinition('d6_node_translation:story'), "Node translation migrations exist after content_translation installed");
}
}

View File

@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Kernel\Migrate\d6;
use Drupal\node\NodeInterface;
/**
* Node content revisions migration.
*
* @group migrate_drupal_6
*/
class MigrateNodeRevisionTest extends MigrateNodeTestBase {
/**
* The entity storage for node.
*
* @var \Drupal\Core\Entity\RevisionableStorageInterface
*/
protected $nodeStorage;
/**
* {@inheritdoc}
*/
protected static $modules = ['language', 'content_translation', 'menu_ui'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->executeMigrations(['d6_node', 'd6_node_revision']);
$this->nodeStorage = $this->container->get('entity_type.manager')
->getStorage('node');
}
/**
* Asserts various aspects of a node revision.
*
* @param int $id
* The revision ID.
* @param string $langcode
* The revision language.
* @param string $title
* The expected title.
* @param string|null $log
* The revision log message.
* @param int $timestamp
* The revision's time stamp.
*
* @internal
*/
protected function assertRevision(int $id, string $langcode, string $title, ?string $log, int $timestamp): void {
/** @var \Drupal\node\NodeInterface $revision */
$revision = $this->nodeStorage->loadRevision($id)
->getTranslation($langcode);
$this->assertInstanceOf(NodeInterface::class, $revision);
$this->assertSame($title, $revision->getTitle());
$this->assertSame($log, $revision->revision_log->value);
$this->assertSame($timestamp, (int) $revision->getRevisionCreationTime());
}
/**
* Tests node revisions migration from Drupal 6 to 8.
*/
public function testNodeRevision(): void {
$node = \Drupal::entityTypeManager()->getStorage('node')->loadRevision(2001);
/** @var \Drupal\node\NodeInterface $node */
$this->assertSame('1', $node->id());
$this->assertSame('2001', $node->getRevisionId());
$this->assertSame('und', $node->langcode->value);
$this->assertSame('Test title rev 3', $node->getTitle());
$this->assertSame('body test rev 3', $node->body->value);
$this->assertSame('teaser test rev 3', $node->body->summary);
$this->assertSame('2', $node->getRevisionUser()->id());
$this->assertSame('modified rev 3', $node->revision_log->value);
$this->assertSame('1420861423', $node->getRevisionCreationTime());
$this->assertRevision(1, 'und', 'Test title', NULL, 1390095702);
$this->assertRevision(3, 'und', 'Test title rev 3', NULL, 1420718386);
$this->assertRevision(4, 'und', 'Test page title rev 4', NULL, 1390095701);
$this->assertRevision(5, 'und', 'Test title rev 2', 'modified rev 2', 1390095703);
$this->assertRevision(6, 'und', 'Node 4', NULL, 1390095701);
$this->assertRevision(7, 'und', 'Node 5', NULL, 1390095701);
$this->assertRevision(8, 'und', 'Node 6', NULL, 1390095701);
$this->assertRevision(9, 'und', 'Node 7', NULL, 1390095701);
$this->assertRevision(10, 'und', 'Node 8', NULL, 1390095701);
$this->assertRevision(11, 'und', 'Node 9', NULL, 1390095701);
$this->assertRevision(12, 'und', 'Once upon a time', NULL, 1444671588);
$this->assertRevision(13, 'en', 'The Real McCoy', NULL, 1444238808);
$this->assertRevision(15, 'zu', 'Abantu zulu', NULL, 1444238808);
$this->assertRevision(17, 'und', 'United Federation of Planets', NULL, 1493066668);
$this->assertRevision(18, 'und', 'Klingon Empire', NULL, 1493066677);
$this->assertRevision(19, 'und', 'Romulan Empire', NULL, 1493066684);
$this->assertRevision(20, 'und', 'Ferengi Commerce Authority', NULL, 1493066693);
$this->assertRevision(21, 'und', 'Ambassador Sarek', NULL, 1494966544);
$this->assertRevision(22, 'und', 'New Forum Topic', NULL, 1501955771);
$this->assertRevision(2001, 'und', 'Test title rev 3', 'modified rev 3', 1420861423);
$this->assertRevision(2002, 'en', 'John Smith - EN', NULL, 1534014650);
// Test that the revision translations are not migrated and there should not
// be a revision with id of 2003.
$ids = [2, 14, 16, 23, 2003];
foreach ($ids as $id) {
$this->assertNull($this->nodeStorage->loadRevision($id));
}
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Kernel\Migrate\d6;
use Drupal\Core\Field\Entity\BaseFieldOverride;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
/**
* @group migrate_drupal_6
*/
class MigrateNodeSettingPromoteTest extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'text', 'menu_ui'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(['node']);
$this->executeMigration('d6_node_type');
$this->executeMigration('d6_node_setting_promote');
}
/**
* Tests migration of the promote checkbox's settings.
*/
public function testMigration(): void {
$this->assertSame('Promoted to front page', BaseFieldOverride::load('node.article.promote')->label());
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Kernel\Migrate\d6;
use Drupal\Core\Field\Entity\BaseFieldOverride;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
/**
* @group migrate_drupal_6
*/
class MigrateNodeSettingStatusTest extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'text', 'menu_ui'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(['node']);
$this->executeMigration('d6_node_type');
$this->executeMigration('d6_node_setting_status');
}
/**
* Tests migration of the publishing status checkbox's settings.
*/
public function testMigration(): void {
$this->assertSame('Publishing status', BaseFieldOverride::load('node.article.status')->label());
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Kernel\Migrate\d6;
use Drupal\Core\Field\Entity\BaseFieldOverride;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
/**
* @group migrate_drupal_6
*/
class MigrateNodeSettingStickyTest extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'text', 'menu_ui'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(['node']);
$this->executeMigration('d6_node_type');
$this->executeMigration('d6_node_setting_sticky');
}
/**
* Tests migration of the sticky checkbox's settings.
*/
public function testMigration(): void {
$this->assertSame('Sticky at the top of lists', BaseFieldOverride::load('node.article.sticky')->label());
}
}

View File

@ -0,0 +1,261 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Kernel\Migrate\d6;
use Drupal\Core\Database\Database;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\node\Entity\Node;
use Drupal\Tests\file\Kernel\Migrate\d6\FileMigrationTestTrait;
/**
* Node content migration.
*
* @group migrate_drupal_6
*/
class MigrateNodeTest extends MigrateNodeTestBase {
use FileMigrationTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'language',
'content_translation',
'menu_ui',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setUpMigratedFiles();
$this->installSchema('file', ['file_usage']);
$this->executeMigrations([
'language',
'd6_language_content_settings',
'd6_node',
'd6_node_translation',
]);
}
/**
* Tests node migration from Drupal 6 to 8.
*/
public function testNode(): void {
// Confirm there are only classic node migration map tables. This shows
// that only the classic migration ran.
$results = $this->nodeMigrateMapTableCount('6');
$this->assertSame(14, $results['node']);
$this->assertSame(0, $results['node_complete']);
$node = Node::load(1);
$this->assertSame('1', $node->id(), 'Node 1 loaded.');
$this->assertSame('und', $node->langcode->value);
$this->assertSame('body test rev 3', $node->body->value);
$this->assertSame('teaser test rev 3', $node->body->summary);
$this->assertSame('filtered_html', $node->body->format);
$this->assertSame('story', $node->getType(), 'Node has the correct bundle.');
$this->assertSame('Test title rev 3', $node->getTitle(), 'Node has the correct title.');
$this->assertSame('1390095702', $node->getCreatedTime(), 'Node has the correct created time.');
$this->assertFalse($node->isSticky());
$this->assertSame('1', $node->getOwnerId());
$this->assertSame('1420861423', $node->getRevisionCreationTime());
/** @var \Drupal\node\NodeInterface $node_revision */
$node_revision = \Drupal::entityTypeManager()->getStorage('node')->loadRevision(2001);
$this->assertSame('Test title rev 3', $node_revision->getTitle());
$this->assertSame('2', $node_revision->getRevisionUser()->id(), 'Node revision has the correct user');
$this->assertSame('1', $node_revision->id(), 'Node 1 loaded.');
$this->assertSame('2001', $node_revision->getRevisionId(), 'Node 1 revision 2001 loaded.');
// This is empty on the first revision.
$this->assertSame('modified rev 3', $node_revision->revision_log->value);
$this->assertSame('This is a shared text field', $node->field_test->value);
$this->assertSame('filtered_html', $node->field_test->format);
$this->assertSame('10', $node->field_test_two->value);
$this->assertSame('20', $node->field_test_two[1]->value);
$this->assertSame('42.42', $node->field_test_three->value, 'Single field second value is correct.');
$this->assertSame('3412', $node->field_test_integer_selectlist[0]->value);
$this->assertSame('1', $node->field_test_identical1->value, 'Integer value is correct');
$this->assertSame('1', $node->field_test_identical2->value, 'Integer value is correct');
$this->assertSame('This is a field with exclude unset.', $node->field_test_exclude_unset->value, 'Field with exclude unset is correct.');
// Test that date fields are migrated.
$this->assertSame('2013-01-02T04:05:00', $node->field_test_date->value, 'Date field is correct');
$this->assertSame('1391357160', $node->field_test_datestamp->value, 'Datestamp field is correct');
$this->assertSame('2015-03-04T06:07:00', $node->field_test_datetime->value, 'Datetime field is correct');
// Test that link fields are migrated.
$this->assertSame('https://www.drupal.org/project/drupal', $node->field_test_link->uri);
$this->assertSame('Drupal project page', $node->field_test_link->title);
$this->assertSame(['target' => '_blank'], $node->field_test_link->options['attributes']);
// Test the file field meta.
$this->assertSame('desc', $node->field_test_filefield->description);
$this->assertSame('4', $node->field_test_filefield->target_id);
// Test that an email field is migrated.
$this->assertSame('PrincessRuwenne@example.com', $node->field_test_email->value);
// Test that node reference field values were migrated.
$node = Node::load(18);
$this->assertCount(2, $node->field_company);
$this->assertSame('Klingon Empire', $node->field_company[0]->entity->label());
$this->assertSame('Romulan Empire', $node->field_company[1]->entity->label());
$this->assertCount(1, $node->field_company_2);
$this->assertSame('Klingon Empire', $node->field_company_2[0]->entity->label());
$this->assertCount(1, $node->field_company_3);
$this->assertSame('Romulan Empire', $node->field_company_3[0]->entity->label());
// Test that user reference field values were migrated.
$this->assertCount(1, $node->field_commander);
$this->assertSame('joe.roe', $node->field_commander[0]->entity->getAccountName());
$node = Node::load(2);
$this->assertSame('Test title rev 3', $node->getTitle());
$this->assertSame('test rev 3', $node->body->value);
$this->assertSame('filtered_html', $node->body->format);
// Test that a link field with an external link is migrated.
$this->assertSame('http://groups.drupal.org/', $node->field_test_link->uri);
$this->assertSame('Drupal Groups', $node->field_test_link->title);
$this->assertSame([], $node->field_test_link->options['attributes']);
$node = Node::load(3);
// Test multivalue field.
$value_1 = $node->field_multivalue->value;
$value_2 = $node->field_multivalue[1]->value;
// SQLite does not support scales for float data types so we need to convert
// the value manually.
if ($this->container->get('database')->driver() == 'sqlite') {
$value_1 = sprintf('%01.2f', $value_1);
$value_2 = sprintf('%01.2f', $value_2);
}
$this->assertSame('33.00', $value_1);
$this->assertSame('44.00', $value_2);
// Test that a link field with an internal link is migrated.
$node = Node::load(9);
$this->assertSame('internal:/node/10', $node->field_test_link->uri);
$this->assertSame('Buy it now', $node->field_test_link->title);
$this->assertSame(['attributes' => ['target' => '_blank']], $node->field_test_link->options);
// Test that translations are working.
$node = Node::load(10);
$this->assertSame('en', $node->langcode->value);
$this->assertSame('The Real McCoy', $node->title->value);
$this->assertTrue($node->hasTranslation('fr'), "Node 10 has french translation");
// Test that content_translation_source is set.
$manager = $this->container->get('content_translation.manager');
$this->assertSame('en', $manager->getTranslationMetadata($node->getTranslation('fr'))->getSource());
// Test that content_translation_source for a source other than English.
$node = Node::load(12);
$this->assertSame('zu', $manager->getTranslationMetadata($node->getTranslation('en'))->getSource());
// Node 11 is a translation of node 10, and should not be imported
// separately.
$this->assertNull(Node::load(11), "Node 11 doesn't exist in D8, it was a translation");
// Rerun migration with two source database changes.
// 1. Add an invalid link attributes and a different URL and
// title. If only the attributes are changed the error does not occur.
Database::getConnection('default', 'migrate')
->update('content_type_story')
->fields([
'field_test_link_url' => 'https://www.drupal.org/node/2127611',
'field_test_link_title' => 'Migrate API in Drupal 8',
'field_test_link_attributes' => '',
])
->condition('nid', '2')
->condition('vid', '3')
->execute();
// 2. Add a leading slash to an internal link.
Database::getConnection('default', 'migrate')
->update('content_type_story')
->fields([
'field_test_link_url' => '/node/10',
])
->condition('nid', '9')
->condition('vid', '12')
->execute();
$this->rerunMigration();
$node = Node::load(2);
$this->assertSame('https://www.drupal.org/node/2127611', $node->field_test_link->uri);
$this->assertSame('Migrate API in Drupal 8', $node->field_test_link->title);
$this->assertSame([], $node->field_test_link->options['attributes']);
$node = Node::load(9);
$this->assertSame('internal:/node/10', $node->field_test_link->uri);
$this->assertSame('Buy it now', $node->field_test_link->title);
$this->assertSame(['attributes' => ['target' => '_blank']], $node->field_test_link->options);
// Test that we can re-import using the EntityContentBase destination.
$title = $this->rerunMigration();
$node = Node::load(2);
$this->assertSame($title, $node->getTitle());
// Test multi-column fields are correctly upgraded.
$this->assertSame('test rev 3', $node->body->value);
$this->assertSame('full_html', $node->body->format);
// Now insert a row indicating a failure and set to update later.
$title = $this->rerunMigration([
'sourceid1' => 2,
'destid1' => NULL,
'source_row_status' => MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
]);
$node = Node::load(2);
$this->assertSame($title, $node->getTitle());
// Test synchronized field.
$value = 'jsmith@example.com';
$node = Node::load(21);
$this->assertSame($value, $node->field_sync->value);
$this->assertArrayNotHasKey('field_sync', $node->getTranslatableFields());
$node = $node->getTranslation('fr');
$this->assertSame($value, $node->field_sync->value);
}
/**
* Execute the migration a second time.
*
* @param array $new_row
* An optional row to be inserted into the id map.
*
* @return string
* The new title in the source for vid 3.
*/
protected function rerunMigration($new_row = []) {
$title = $this->randomString();
$source_connection = Database::getConnection('default', 'migrate');
$source_connection->update('node_revisions')
->fields([
'title' => $title,
'format' => 2,
])
->condition('vid', 3)
->execute();
$migration = $this->getMigration('d6_node:story');
$table_name = $migration->getIdMap()->mapTableName();
$default_connection = \Drupal::database();
$default_connection->truncate($table_name)->execute();
if ($new_row) {
$hash = $migration->getIdMap()->getSourceIdsHash(['nid' => $new_row['sourceid1']]);
$new_row['source_ids_hash'] = $hash;
$default_connection->insert($table_name)
->fields($new_row)
->execute();
}
$this->executeMigration($migration);
return $title;
}
}

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Kernel\Migrate\d6;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
use Drupal\user\Entity\User;
/**
* Base class for Node migration tests.
*/
abstract class MigrateNodeTestBase extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('node');
$this->installConfig(['node']);
$this->installSchema('node', ['node_access']);
// Create a new user which needs to have UID 1, because that is expected by
// the assertions from
// \Drupal\migrate_drupal\Tests\d6\MigrateNodeRevisionTest.
User::create([
'uid' => 1,
'name' => $this->randomMachineName(),
'status' => 1,
])->enforceIsNew()->save();
$this->migrateUsers(FALSE);
$this->migrateFields();
$this->executeMigration('d6_node_settings');
}
}

View File

@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Kernel\Migrate\d6;
use Drupal\field\Entity\FieldConfig;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
use Drupal\node\Entity\NodeType;
/**
* Upgrade node types to node.type.*.yml.
*
* @group migrate_drupal_6
*/
class MigrateNodeTypeTest extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['menu_ui'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(['node']);
$this->executeMigration('d6_node_type');
}
/**
* Tests Drupal 6 node type to Drupal 8 migration.
*/
public function testNodeType(): void {
$id_map = $this->getMigration('d6_node_type')->getIdMap();
// Test the test_page content type.
$node_type_page = NodeType::load('test_page');
$this->assertSame('test_page', $node_type_page->id(), 'Node type test_page loaded');
$this->assertTrue($node_type_page->displaySubmitted());
$this->assertFalse($node_type_page->shouldCreateNewRevision());
$this->assertSame(DRUPAL_OPTIONAL, $node_type_page->getPreviewMode());
$this->assertSame($id_map->lookupDestinationIds(['test_page']), [['test_page']]);
// Test we have a body field.
$field = FieldConfig::loadByName('node', 'test_page', 'body');
$this->assertSame('This is the body field label', $field->getLabel(), 'Body field was found.');
// Test default menus.
$expected_available_menus = ['tools'];
$this->assertSame($expected_available_menus, $node_type_page->getThirdPartySetting('menu_ui', 'available_menus'));
$expected_parent = 'tools:';
$this->assertSame($expected_parent, $node_type_page->getThirdPartySetting('menu_ui', 'parent'));
// Test the test_story content type.
$node_type_story = NodeType::load('test_story');
$this->assertSame('test_story', $node_type_story->id(), 'Node type test_story loaded');
$this->assertTrue($node_type_story->displaySubmitted());
$this->assertFalse($node_type_story->shouldCreateNewRevision());
$this->assertSame(DRUPAL_OPTIONAL, $node_type_story->getPreviewMode());
$this->assertSame([['test_story']], $id_map->lookupDestinationIds(['test_story']));
// Test we don't have a body field.
$field = FieldConfig::loadByName('node', 'test_story', 'body');
$this->assertNull($field, 'No body field found');
// Test default menus.
$expected_available_menus = ['tools'];
$this->assertSame($expected_available_menus, $node_type_story->getThirdPartySetting('menu_ui', 'available_menus'));
$expected_parent = 'tools:';
$this->assertSame($expected_parent, $node_type_story->getThirdPartySetting('menu_ui', 'parent'));
// Test the test_event content type.
$node_type_event = NodeType::load('test_event');
$this->assertSame('test_event', $node_type_event->id(), 'Node type test_event loaded');
$this->assertTrue($node_type_event->displaySubmitted());
$this->assertTrue($node_type_event->shouldCreateNewRevision());
$this->assertSame(DRUPAL_OPTIONAL, $node_type_event->getPreviewMode());
$this->assertSame([['test_event']], $id_map->lookupDestinationIds(['test_event']));
// Test we have a body field.
$field = FieldConfig::loadByName('node', 'test_event', 'body');
$this->assertSame('Body', $field->getLabel(), 'Body field was found.');
$expected_available_menus = ['tools'];
$this->assertSame($expected_available_menus, $node_type_event->getThirdPartySetting('menu_ui', 'available_menus'));
$expected_parent = 'tools:';
$this->assertSame($expected_parent, $node_type_event->getThirdPartySetting('menu_ui', 'parent'));
// Test the 32 character type name exists.
$node_type = NodeType::load('a_thirty_two_character_type_name');
$this->assertSame('a_thirty_two_character_type_name', $node_type->id());
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Kernel\Migrate\d6;
use Drupal\Core\Entity\Entity\EntityViewMode;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
/**
* Migrate view modes.
*
* @group migrate_drupal_6
*/
class MigrateViewModesTest extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->executeMigration('d6_view_modes');
}
/**
* Tests Drupal 6 view modes to Drupal 8 migration.
*/
public function testViewModes(): void {
// Test a new view mode.
$view_mode = EntityViewMode::load('node.preview');
$this->assertNotNull($view_mode);
$this->assertSame('Preview', $view_mode->label(), 'View mode has correct label.');
// Test the ID map.
$this->assertSame([['node', 'preview']], $this->getMigration('d6_view_modes')->getIdMap()->lookupDestinationIds([1]));
}
}

View File

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Kernel\Migrate\d6;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Symfony\Component\HttpFoundation\Request;
/**
* Tests node translation redirects.
*
* @group migrate_drupal
* @group node
*/
class NodeTranslationRedirectTest extends MigrateDrupal6TestBase {
use UserCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'content_translation',
'language',
'menu_ui',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setUpCurrentUser();
$this->installEntitySchema('node');
$this->installConfig(['node']);
$this->installSchema('node', ['node_access']);
$this->migrateUsers(FALSE);
$this->migrateFields();
$this->executeMigrations([
'language',
'd6_language_types',
'd6_language_negotiation_settings',
'd6_node_settings',
'd6_node',
'd6_node_translation',
]);
}
/**
* Tests that not found node translations are redirected.
*/
public function testNodeTranslationRedirect(): void {
$kernel = $this->container->get('http_kernel');
$request = Request::create('/node/11');
$response = $kernel->handle($request);
$this->assertSame(301, $response->getStatusCode());
$this->assertSame('/node/10', $response->getTargetUrl());
}
}

Some files were not shown because too many files have changed in this diff Show More