first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\Tests\views_ui\Functional\UITestBase;
/**
* Tests views role access plugin UI.
*
* @group user
* @see \Drupal\user\Plugin\views\access\Role
*/
class AccessRoleUITest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_access_role'];
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['user', 'user_test_views'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE, $modules = ['user_test_views']): void {
parent::setUp($import_test_views, $modules);
}
/**
* Tests the role access plugin UI.
*/
public function testAccessRoleUI(): void {
$entity_type_manager = $this->container->get('entity_type.manager');
$entity_type_manager->getStorage('user_role')->create(['id' => 'custom_role', 'label' => 'Custom role'])->save();
$access_url = "admin/structure/views/nojs/display/test_access_role/default/access_options";
$this->drupalGet($access_url);
$this->submitForm(['access_options[role][custom_role]' => 1], 'Apply');
$this->assertSession()->statusCodeEquals(200);
$this->submitForm([], 'Save');
$view = $entity_type_manager->getStorage('view')->load('test_access_role');
$display = $view->getDisplay('default');
$this->assertEquals(['custom_role' => 'custom_role'], $display['display_options']['access']['options']['role']);
// Test changing access plugin from role to none.
$this->drupalGet('admin/structure/views/nojs/display/test_access_role/default/access');
$this->submitForm(['access[type]' => 'none'], 'Apply');
$this->submitForm([], 'Save');
// Verify that role option is not set.
$view = $entity_type_manager->getStorage('view')->load('test_access_role');
$display = $view->getDisplay('default');
$this->assertFalse(isset($display['display_options']['access']['options']['role']));
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for user.
*
* @group user
*/
class GenericTest extends GenericModuleTestBase {}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group rest
*/
class RoleJsonAnonTest extends RoleResourceTestBase {
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\user\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
*/
class RoleJsonBasicAuthTest extends RoleResourceTestBase {
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\user\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group rest
*/
class RoleJsonCookieTest extends RoleResourceTestBase {
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,72 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional\Rest;
use Drupal\Tests\rest\Functional\EntityResource\ConfigEntityResourceTestBase;
use Drupal\user\Entity\Role;
abstract class RoleResourceTestBase extends ConfigEntityResourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['user'];
/**
* {@inheritdoc}
*/
protected static $entityTypeId = 'user_role';
/**
* @var \Drupal\user\RoleInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
$this->grantPermissionsToTestedRole(['administer permissions']);
}
/**
* {@inheritdoc}
*/
protected function createEntity() {
$role = Role::create([
'id' => 'llama',
'label' => 'Llama',
]);
$role->save();
return $role;
}
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
return [
'uuid' => $this->entity->uuid(),
'weight' => 2,
'langcode' => 'en',
'status' => TRUE,
'dependencies' => [],
'id' => 'llama',
'label' => 'Llama',
'is_admin' => NULL,
'permissions' => [],
];
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPostEntity() {
// @todo Update in https://www.drupal.org/node/2300677.
return [];
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class RoleXmlAnonTest extends RoleResourceTestBase {
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\user\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class RoleXmlBasicAuthTest extends RoleResourceTestBase {
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\user\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class RoleXmlCookieTest extends RoleResourceTestBase {
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,32 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group rest
* @group #slow
*/
class UserJsonAnonTest extends UserResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
* @group #slow
*/
class UserJsonBasicAuthTest extends UserResourceTestBase {
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,37 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group rest
* @group #slow
*/
class UserJsonCookieTest extends UserResourceTestBase {
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,350 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional\Rest;
use Drupal\Core\Url;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
use Drupal\user\Entity\User;
use GuzzleHttp\RequestOptions;
abstract class UserResourceTestBase extends EntityResourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['user'];
/**
* {@inheritdoc}
*/
protected static $entityTypeId = 'user';
/**
* {@inheritdoc}
*/
protected static $patchProtectedFieldNames = [
'changed' => NULL,
];
/**
* @var \Drupal\user\UserInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
protected static $labelFieldName = 'name';
/**
* {@inheritdoc}
*/
protected static $firstCreatedEntityId = 4;
/**
* {@inheritdoc}
*/
protected static $secondCreatedEntityId = 5;
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
switch ($method) {
case 'GET':
$this->grantPermissionsToTestedRole(['access user profiles']);
break;
case 'POST':
case 'PATCH':
case 'DELETE':
$this->grantPermissionsToTestedRole(['administer users']);
break;
}
}
/**
* {@inheritdoc}
*/
protected function createEntity() {
// Create a "Llama" user.
$user = User::create(['created' => 123456789]);
$user->setUsername('Llama')
->setChangedTime(123456789)
->activate()
->save();
return $user;
}
/**
* {@inheritdoc}
*/
protected function createAnotherEntity() {
/** @var \Drupal\user\UserInterface $user */
$user = $this->entity->createDuplicate();
$user->setUsername($user->label() . '_dupe');
$user->save();
return $user;
}
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
return [
'uid' => [
['value' => 3],
],
'uuid' => [
['value' => $this->entity->uuid()],
],
'langcode' => [
[
'value' => 'en',
],
],
'name' => [
[
'value' => 'Llama',
],
],
'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,
],
],
'default_langcode' => [
[
'value' => TRUE,
],
],
];
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPostEntity() {
return [
'name' => [
[
'value' => 'Drama llama',
],
],
];
}
/**
* Tests PATCHing security-sensitive base fields of the logged in account.
*/
public function testPatchDxForSecuritySensitiveBaseFields(): void {
// The anonymous user is never allowed to modify itself.
if (!static::$auth) {
$this->markTestSkipped();
}
$this->initAuthentication();
$this->provisionEntityResource();
/** @var \Drupal\user\UserInterface $user */
$user = static::$auth ? $this->account : User::load(0);
// @todo Remove the array_diff_key() call in https://www.drupal.org/node/2821077.
$original_normalization = array_diff_key($this->serializer->normalize($user, static::$format), ['created' => TRUE, 'changed' => TRUE, 'name' => TRUE]);
// Since this test must be performed by the user that is being modified,
// we cannot use $this->getUrl().
$url = $user->toUrl()->setOption('query', ['_format' => static::$format]);
$request_options = [
RequestOptions::HEADERS => ['Content-Type' => static::$mimeType],
];
$request_options = array_merge_recursive($request_options, $this->getAuthenticationRequestOptions('PATCH'));
// Test case 1: changing email.
$normalization = $original_normalization;
$normalization['mail'] = [['value' => 'new-email@example.com']];
$request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
// DX: 422 when changing email without providing the password.
$response = $this->request('PATCH', $url, $request_options);
$this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n", $response, FALSE, FALSE, FALSE, FALSE);
$normalization['pass'] = [['existing' => 'wrong']];
$request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
// DX: 422 when changing email while providing a wrong password.
$response = $this->request('PATCH', $url, $request_options);
$this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n", $response, FALSE, FALSE, FALSE, FALSE);
$normalization['pass'] = [['existing' => $this->account->passRaw]];
$request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
// 200 for well-formed request.
$response = $this->request('PATCH', $url, $request_options);
$this->assertResourceResponse(200, FALSE, $response);
// Test case 2: changing password.
$normalization = $original_normalization;
$new_password = $this->randomString();
$normalization['pass'] = [['value' => $new_password]];
$request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
// DX: 422 when changing password without providing the current password.
$response = $this->request('PATCH', $url, $request_options);
$this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\npass: Your current password is missing or incorrect; it's required to change the Password.\n", $response, FALSE, FALSE, FALSE, FALSE);
$normalization['pass'][0]['existing'] = $this->account->pass_raw;
$request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
// 200 for well-formed request.
$response = $this->request('PATCH', $url, $request_options);
$this->assertResourceResponse(200, FALSE, $response);
// Verify that we can log in with the new password.
$this->assertRpcLogin($user->getAccountName(), $new_password);
// Update password in $this->account, prepare for future requests.
$this->account->passRaw = $new_password;
$this->initAuthentication();
$request_options = [
RequestOptions::HEADERS => ['Content-Type' => static::$mimeType],
];
$request_options = array_merge_recursive($request_options, $this->getAuthenticationRequestOptions('PATCH'));
// Test case 3: changing name.
$normalization = $original_normalization;
$normalization['name'] = [['value' => 'Cooler Llama']];
$request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
// DX: 403 when modifying username without required permission.
$response = $this->request('PATCH', $url, $request_options);
$this->assertResourceErrorResponse(403, "Access denied on updating field 'name'.", $response);
$this->grantPermissionsToTestedRole(['change own username']);
// 200 for well-formed request.
$response = $this->request('PATCH', $url, $request_options);
$this->assertResourceResponse(200, FALSE, $response);
// Verify that we can log in with the new username.
$this->assertRpcLogin('Cooler Llama', $new_password);
}
/**
* Verifies that logging in with the given username and password works.
*
* @param string $username
* The username to log in with.
* @param string $password
* The password to log in with.
*/
protected function assertRpcLogin($username, $password) {
$request_body = [
'name' => $username,
'pass' => $password,
];
$request_options = [
RequestOptions::HEADERS => [],
RequestOptions::BODY => $this->serializer->encode($request_body, 'json'),
];
$response = $this->request('POST', Url::fromRoute('user.login.http')->setRouteParameter('_format', 'json'), $request_options);
$this->assertSame(200, $response->getStatusCode());
}
/**
* Tests PATCHing security-sensitive base fields to change other users.
*/
public function testPatchSecurityOtherUser(): void {
// The anonymous user is never allowed to modify other users.
if (!static::$auth) {
$this->markTestSkipped();
}
$this->initAuthentication();
$this->provisionEntityResource();
/** @var \Drupal\user\UserInterface $user */
$user = $this->account;
$original_normalization = array_diff_key($this->serializer->normalize($user, static::$format), ['changed' => TRUE]);
// Since this test must be performed by the user that is being modified,
// we cannot use $this->getUrl().
$url = $user->toUrl()->setOption('query', ['_format' => static::$format]);
$request_options = [
RequestOptions::HEADERS => ['Content-Type' => static::$mimeType],
];
$request_options = array_merge_recursive($request_options, $this->getAuthenticationRequestOptions('PATCH'));
$normalization = $original_normalization;
$normalization['mail'] = [['value' => 'new-email@example.com']];
$request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
// Try changing user 1's email.
$user1 = [
'mail' => [['value' => 'another_email_address@example.com']],
'uid' => [['value' => 1]],
'name' => [['value' => 'another_user_name']],
'pass' => [['existing' => $this->account->passRaw]],
'uuid' => [['value' => '2e9403a4-d8af-4096-a116-624710140be0']],
] + $original_normalization;
$request_options[RequestOptions::BODY] = $this->serializer->encode($user1, static::$format);
$response = $this->request('PATCH', $url, $request_options);
// Ensure the email address has not changed.
$this->assertEquals('admin@example.com', $this->entityStorage->loadUnchanged(1)->getEmail());
$this->assertResourceErrorResponse(403, "Access denied on updating field 'uid'. The entity ID cannot be changed.", $response);
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
switch ($method) {
case 'GET':
return "The 'access user profiles' permission is required.";
case 'PATCH':
return "Users can only update their own account, unless they have the 'administer users' permission.";
case 'DELETE':
return "The 'cancel account' permission is required.";
default:
return parent::getExpectedUnauthorizedAccessMessage($method);
}
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedEntityAccessCacheability($is_authenticated) {
// @see \Drupal\user\UserAccessControlHandler::checkAccess()
$result = parent::getExpectedUnauthorizedEntityAccessCacheability($is_authenticated);
if (!\Drupal::currentUser()->hasPermission('access user profiles')) {
$result->addCacheContexts(['user']);
}
return $result;
}
/**
* {@inheritdoc}
*/
protected function getExpectedCacheContexts() {
return [
'url.site',
// Due to the 'mail' field's access varying by user.
'user',
];
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
* @group #slow
*/
class UserXmlAnonTest extends UserResourceTestBase {
use AnonResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
public function testPatchDxForSecuritySensitiveBaseFields(): void {
// Deserialization of the XML format is not supported.
$this->markTestSkipped();
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
* @group #slow
*/
class UserXmlBasicAuthTest extends UserResourceTestBase {
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';
/**
* {@inheritdoc}
*/
public function testPatchDxForSecuritySensitiveBaseFields(): void {
// Deserialization of the XML format is not supported.
$this->markTestSkipped();
}
/**
* {@inheritdoc}
*/
public function testPatchSecurityOtherUser(): void {
// Deserialization of the XML format is not supported.
$this->markTestSkipped();
}
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
* @group #slow
*/
class UserXmlCookieTest extends UserResourceTestBase {
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';
/**
* {@inheritdoc}
*/
public function testPatchDxForSecuritySensitiveBaseFields(): void {
// Deserialization of the XML format is not supported.
$this->markTestSkipped();
}
/**
* {@inheritdoc}
*/
public function testPatchSecurityOtherUser(): void {
// Deserialization of the XML format is not supported.
$this->markTestSkipped();
}
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional\Update;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
use Drupal\user\Entity\Role;
/**
* Tests user_update_10000() upgrade path.
*
* @group Update
* @group legacy
*/
class UserUpdateRoleMigrateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../../system/tests/fixtures/update/drupal-9.4.0.bare.standard.php.gz',
];
}
/**
* Tests that roles have only existing permissions.
*/
public function testRolePermissions(): void {
/** @var \Drupal\Core\Database\Connection $connection */
$connection = \Drupal::service('database');
// Edit the authenticated role to have a non-existent permission.
$authenticated = $connection->select('config')
->fields('config', ['data'])
->condition('collection', '')
->condition('name', 'user.role.authenticated')
->execute()
->fetchField();
$authenticated = unserialize($authenticated);
$authenticated['permissions'][] = 'does_not_exist';
$connection->update('config')
->fields([
'data' => serialize($authenticated),
])
->condition('collection', '')
->condition('name', 'user.role.authenticated')
->execute();
$authenticated = Role::load('authenticated');
$this->assertTrue($authenticated->hasPermission('does_not_exist'), 'Authenticated role has a permission that does not exist');
$this->runUpdates();
$this->assertSession()->pageTextContains('The role Authenticated user has had non-existent permissions removed. Check the logs for details.');
$authenticated = Role::load('authenticated');
$this->assertFalse($authenticated->hasPermission('does_not_exist'), 'Authenticated role does not have a permission that does not exist');
$this->drupalLogin($this->createUser(['access site reports']));
$this->drupalGet('admin/reports/dblog', ['query' => ['type[]' => 'update']]);
$this->clickLink('The role Authenticated user has had the following non-…');
$this->assertSession()->pageTextContains('The role Authenticated user has had the following non-existent permission(s) removed: does_not_exist.');
}
}

View File

@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests user-account links.
*
* @group user
*/
class UserAccountLinksTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['menu_ui', 'block', 'test_page_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalPlaceBlock('system_menu_block:account', ['id' => 'user_account_links_test_system_menu_block_account']);
// Make test-page default.
$this->config('system.site')->set('page.front', '/test-page')->save();
}
/**
* Tests the secondary menu.
*/
public function testSecondaryMenu(): void {
// Create a regular user.
$user = $this->drupalCreateUser([]);
// Log in and get the homepage.
$this->drupalLogin($user);
$this->drupalGet('<front>');
// For a logged-in user, expect the secondary menu to have links for "My
// account" and "Log out".
$this->assertSession()->elementsCount('xpath', '//nav[@id="block-user-account-links-test-system-menu-block-account"]/ul/li/a[contains(@href, "user") and text()="My account"]', 1);
$this->assertSession()->elementsCount('xpath', '//nav[@id="block-user-account-links-test-system-menu-block-account"]/ul/li/a[contains(@href, "user/logout") and text()="Log out"]', 1);
// Log out and get the homepage.
$this->drupalLogout();
$this->drupalGet('<front>');
// For a logged-out user, expect the secondary menu to have a "Log in" link.
$this->assertSession()->elementsCount('xpath', '//nav[@id="block-user-account-links-test-system-menu-block-account"]/ul/li/a[contains(@href, "user/login") and text()="Log in"]', 1);
}
/**
* Tests disabling the 'My account' link.
*/
public function testDisabledAccountLink(): void {
// Create an admin user and log in.
$this->drupalLogin($this->drupalCreateUser([
'access administration pages',
'administer menu',
]));
// Verify that the 'My account' link exists before we check for its
// disappearance.
$this->assertSession()->elementsCount('xpath', '//nav[@id="block-user-account-links-test-system-menu-block-account"]/ul/li/a[contains(@href, "user") and text()="My account"]', 1);
// Verify that the 'My account' link is enabled. Do not assume the value of
// auto-increment is 1. Use XPath to obtain input element id and name using
// the consistent label text.
$this->drupalGet('admin/structure/menu/manage/account');
$label = $this->xpath('//label[contains(.,:text)]/@for', [':text' => 'Enable My account menu link']);
$this->assertSession()->checkboxChecked($label[0]->getText());
// Disable the 'My account' link.
$edit['links[menu_plugin_id:user.page][enabled]'] = FALSE;
$this->drupalGet('admin/structure/menu/manage/account');
$this->submitForm($edit, 'Save');
// Get the homepage.
$this->drupalGet('<front>');
// Verify that the 'My account' link does not appear when disabled.
$this->assertSession()->elementNotExists('xpath', '//nav[@id="block-user-account-links-test-system-menu-block-account"]/ul/li/a[contains(@href, "user") and text()="My account"]');
}
/**
* Tests page title is set correctly on user account tabs.
*/
public function testAccountPageTitles(): void {
// Default page titles are suffixed with the site name - Drupal.
$title_suffix = ' | Drupal';
$this->drupalGet('user');
$this->assertSession()->titleEquals('Log in' . $title_suffix);
$this->drupalGet('user/login');
$this->assertSession()->titleEquals('Log in' . $title_suffix);
$this->drupalGet('user/register');
$this->assertSession()->titleEquals('Create new account' . $title_suffix);
$this->drupalGet('user/password');
$this->assertSession()->titleEquals('Reset your password' . $title_suffix);
// Check the page title for registered users is "My Account" in menus.
$this->drupalLogin($this->drupalCreateUser());
// After login, the client is redirected to /user.
$this->assertSession()->linkExists('My account', 0, "Page title of /user is 'My Account' in menus for registered users");
$this->assertSession()->linkByHrefExists(\Drupal::urlGenerator()->generate('user.page'), 0);
}
/**
* Ensures that logout URL redirects an anonymous user to the front page.
*/
public function testAnonymousLogout(): void {
$this->drupalGet('user/logout');
$this->assertSession()->addressEquals('/');
$this->assertSession()->statusCodeEquals(200);
// The redirection shouldn't affect other pages.
$this->drupalGet('admin');
$this->assertSession()->addressEquals('/admin');
$this->assertSession()->statusCodeEquals(403);
}
}

View File

@@ -0,0 +1,216 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Tests\BrowserTestBase;
/**
* Tests users' ability to change their own administration language.
*
* @group user
*/
class UserAdminLanguageTest extends BrowserTestBase {
/**
* A user with permission to access admin pages and administer languages.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* A non-administrator user for this test.
*
* @var \Drupal\user\UserInterface
*/
protected $regularUser;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['user', 'language', 'language_test', 'user_language_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// User to add and remove language.
$this->adminUser = $this->drupalCreateUser([
'administer languages',
'access administration pages',
]);
// User to check non-admin access.
$this->regularUser = $this->drupalCreateUser();
}
/**
* Tests that admin language is not configurable in single language sites.
*/
public function testUserAdminLanguageConfigurationNotAvailableWithOnlyOneLanguage(): void {
$this->drupalLogin($this->adminUser);
$this->setLanguageNegotiation();
$path = 'user/' . $this->adminUser->id() . '/edit';
$this->drupalGet($path);
// Ensure administration pages language settings widget is not available.
$this->assertSession()->fieldNotExists('edit-preferred-admin-langcode');
}
/**
* Tests that admin language negotiation is configurable only if enabled.
*/
public function testUserAdminLanguageConfigurationAvailableWithAdminLanguageNegotiation(): void {
$this->drupalLogin($this->adminUser);
$this->addCustomLanguage();
$path = 'user/' . $this->adminUser->id() . '/edit';
// Checks with user administration pages language negotiation disabled.
$this->drupalGet($path);
// Ensure administration pages language settings widget is not available.
$this->assertSession()->fieldNotExists('edit-preferred-admin-langcode');
// Checks with user administration pages language negotiation enabled.
$this->setLanguageNegotiation();
$this->drupalGet($path);
// Ensure administration pages language settings widget is available.
$this->assertSession()->fieldExists('edit-preferred-admin-langcode');
}
/**
* Tests that the admin language is configurable only for administrators.
*
* If a user has the permission "access administration pages" or
* "view the administration theme", they should be able to see the setting to
* pick the language they want those pages in.
*
* If a user does not have that permission, it would confusing for them to
* have a setting for pages they cannot access, so they should not be able to
* set a language for those pages.
*/
public function testUserAdminLanguageConfigurationAvailableIfAdminLanguageNegotiationIsEnabled(): void {
$this->drupalLogin($this->adminUser);
// Adds a new language, because with only one language, setting won't show.
$this->addCustomLanguage();
$this->setLanguageNegotiation();
$path = 'user/' . $this->adminUser->id() . '/edit';
$this->drupalGet($path);
// Ensure administration pages language setting is visible for admin.
$this->assertSession()->fieldExists('edit-preferred-admin-langcode');
// Ensure administration pages language setting is visible for editors.
$editor = $this->drupalCreateUser(['view the administration theme']);
$this->drupalLogin($editor);
$path = 'user/' . $editor->id() . '/edit';
$this->drupalGet($path);
$this->assertSession()->fieldExists('edit-preferred-admin-langcode');
// Ensure administration pages language setting is hidden for non-admins.
$this->drupalLogin($this->regularUser);
$path = 'user/' . $this->regularUser->id() . '/edit';
$this->drupalGet($path);
$this->assertSession()->fieldNotExists('edit-preferred-admin-langcode');
}
/**
* Tests the actual language negotiation.
*/
public function testActualNegotiation(): void {
$this->drupalLogin($this->adminUser);
$this->addCustomLanguage();
$this->setLanguageNegotiation();
// Even though we have admin language negotiation, so long as the user has
// no preference set, negotiation will fall back further.
$path = 'user/' . $this->adminUser->id() . '/edit';
$this->drupalGet($path);
$this->assertSession()->pageTextContains('Language negotiation method: language-default');
$this->drupalGet('xx/' . $path);
$this->assertSession()->pageTextContains('Language negotiation method: language-url');
// Set a preferred language code for the user.
$edit = [];
$edit['preferred_admin_langcode'] = 'xx';
$this->drupalGet($path);
$this->submitForm($edit, 'Save');
// Test negotiation with the URL method first. The admin method will only
// be used if the URL method did not match.
$this->drupalGet($path);
$this->assertSession()->pageTextContains('Language negotiation method: language-user-admin');
$this->drupalGet('xx/' . $path);
$this->assertSession()->pageTextContains('Language negotiation method: language-url');
// Test negotiation with the admin language method first. The admin method
// will be used at all times.
$this->setLanguageNegotiation(TRUE);
$this->drupalGet($path);
$this->assertSession()->pageTextContains('Language negotiation method: language-user-admin');
$this->drupalGet('xx/' . $path);
$this->assertSession()->pageTextContains('Language negotiation method: language-user-admin');
// Make sure 'language-user-admin' plugin does not fail when a route is
// restricted to POST requests and language negotiation with the admin
// language method is used.
$this->drupalGet('/user-language-test/form');
$this->submitForm([], 'Send');
$this->assertSession()->statusCodeEquals(200);
// Unset the preferred language code for the user.
$edit = [];
$edit['preferred_admin_langcode'] = '';
$this->drupalGet($path);
$this->submitForm($edit, 'Save');
$this->drupalGet($path);
$this->assertSession()->pageTextContains('Language negotiation method: language-default');
$this->drupalGet('xx/' . $path);
$this->assertSession()->pageTextContains('Language negotiation method: language-url');
}
/**
* Sets the User interface negotiation detection method.
*
* Enables the "Account preference for administration pages" language
* detection method for the User interface language negotiation type.
*
* @param bool $admin_first
* Whether the admin negotiation should be first.
*/
public function setLanguageNegotiation($admin_first = FALSE) {
$edit = [
'language_interface[enabled][language-user-admin]' => TRUE,
'language_interface[enabled][language-url]' => TRUE,
'language_interface[weight][language-user-admin]' => ($admin_first ? -12 : -8),
'language_interface[weight][language-url]' => -10,
];
$this->drupalGet('admin/config/regional/language/detection');
$this->submitForm($edit, 'Save settings');
}
/**
* Helper method for adding a custom language.
*/
public function addCustomLanguage() {
$langcode = 'xx';
// The English name for the language.
$name = $this->randomMachineName(16);
$edit = [
'predefined_langcode' => 'custom',
'langcode' => $langcode,
'label' => $name,
'direction' => LanguageInterface::DIRECTION_LTR,
];
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm($edit, 'Add custom language');
}
}

View File

@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\user\Entity\User;
/**
* Tests the user admin listing if views is not enabled.
*
* @group user
* @see user_admin_account()
*/
class UserAdminListingTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests the listing.
*/
public function testUserListing(): void {
// Ensure the anonymous user cannot access the admin listing.
$this->drupalGet('admin/people');
$this->assertSession()->statusCodeEquals(403);
// Create a bunch of users.
$accounts = [];
for ($i = 0; $i < 3; $i++) {
$account = $this->drupalCreateUser();
$accounts[$account->label()] = $account;
}
// Create a blocked user.
$account = $this->drupalCreateUser();
$account->block();
$account->save();
$accounts[$account->label()] = $account;
// Create a user at a certain timestamp.
$account = $this->drupalCreateUser();
$account->created = 1363219200;
$account->save();
$accounts[$account->label()] = $account;
$timestamp_user = $account->label();
$rid_1 = $this->drupalCreateRole([], 'custom_role_1', 'custom_role_1');
$rid_2 = $this->drupalCreateRole([], 'custom_role_2', 'custom_role_2');
$account = $this->drupalCreateUser();
$account->addRole($rid_1)->addRole($rid_2)->save();
$accounts[$account->label()] = $account;
$role_account_name = $account->label();
// Create an admin user and look at the listing.
$admin_user = $this->drupalCreateUser(['administer users']);
$accounts[$admin_user->label()] = $admin_user;
$accounts['admin'] = User::load(1);
$this->drupalLogin($admin_user);
// Ensure the admin user can access the admin listing.
$this->drupalGet('admin/people');
$this->assertSession()->statusCodeEquals(200);
$result = $this->xpath('//table[contains(@class, "responsive-enabled")]/tbody/tr');
$result_accounts = [];
foreach ($result as $account) {
$account_columns = $account->findAll('css', 'td');
$name = $account_columns[0]->find('css', 'a')->getText();
$roles = [];
$account_roles = $account_columns[2]->findAll('css', 'td ul li');
if (!empty($account_roles)) {
foreach ($account_roles as $element) {
$roles[] = $element->getText();
}
}
$result_accounts[$name] = [
'name' => $name,
'status' => $account_columns[1]->getText(),
'roles' => $roles,
'member_for' => $account_columns[3]->getText(),
'last_access' => $account_columns[4]->getText(),
];
}
$this->assertEmpty(array_keys(array_diff_key($result_accounts, $accounts)), 'Ensure all accounts are listed.');
foreach ($result_accounts as $name => $values) {
$this->assertEquals($accounts[$name]->status->value, $values['status'] == 'active');
}
$expected_roles = ['custom_role_1', 'custom_role_2'];
$this->assertEquals($expected_roles, $result_accounts[$role_account_name]['roles'], 'Ensure roles are listed properly.');
$this->assertEquals(\Drupal::service('date.formatter')->formatTimeDiffSince($accounts[$timestamp_user]->created->value), $result_accounts[$timestamp_user]['member_for'], 'Ensure the right member time is displayed.');
$this->assertEquals('never', $result_accounts[$timestamp_user]['last_access'], 'Ensure the last access time is "never".');
}
}

View File

@@ -0,0 +1,237 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\Core\Test\AssertMailTrait;
use Drupal\Tests\BrowserTestBase;
use Drupal\user\RoleInterface;
use Drupal\user\UserInterface;
/**
* Tests user administration page functionality.
*
* @group user
*/
class UserAdminTest extends BrowserTestBase {
use AssertMailTrait {
getMails as drupalGetMails;
}
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['taxonomy', 'views'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Gets the xpath selector for a user account.
*
* @param \Drupal\user\UserInterface $user
* The user to get the link for.
*
* @return string
* The xpath selector for the user link.
*/
private static function getLinkSelectorForUser(UserInterface $user): string {
return '//td[contains(@class, "views-field-name")]/a[text()="' . $user->getAccountName() . '"]';
}
/**
* Registers a user and deletes it.
*/
public function testUserAdmin(): void {
$config = $this->config('user.settings');
$user_a = $this->drupalCreateUser();
$user_a->name = 'User A';
$user_a->mail = $this->randomMachineName() . '@example.com';
$user_a->save();
$user_b = $this->drupalCreateUser(['administer taxonomy']);
$user_b->name = 'User B';
$user_b->save();
$user_c = $this->drupalCreateUser(['administer taxonomy']);
$user_c->name = 'User C';
$user_c->save();
$user_storage = $this->container->get('entity_type.manager')->getStorage('user');
// Create admin user to delete registered user.
$admin_user = $this->drupalCreateUser(['administer users']);
// Use a predictable name so that we can reliably order the user admin page
// by name.
$admin_user->name = 'Admin user';
$admin_user->save();
$this->drupalLogin($admin_user);
$this->drupalGet('admin/people');
$this->assertSession()->pageTextContains($user_a->getAccountName());
$this->assertSession()->pageTextContains($user_b->getAccountName());
$this->assertSession()->pageTextContains($user_c->getAccountName());
$this->assertSession()->pageTextContains($admin_user->getAccountName());
// Test for existence of edit link in table.
$link = $user_a->toLink('Edit', 'edit-form', [
'query' => ['destination' => $user_a->toUrl('collection')->toString()],
'attributes' => ['aria-label' => 'Edit ' . $user_a->label()],
])->toString();
$this->assertSession()->responseContains($link);
// Test exposed filter elements.
foreach (['user', 'role', 'permission', 'status'] as $field) {
$this->assertSession()->fieldExists("edit-$field");
}
// Make sure the reduce duplicates element from the ManyToOneHelper is not
// displayed.
$this->assertSession()->fieldNotExists('edit-reduce-duplicates');
// Filter the users by name/email.
$this->drupalGet('admin/people', ['query' => ['user' => $user_a->getAccountName()]]);
$result = $this->xpath('//table/tbody/tr');
$this->assertCount(1, $result, 'Filter by username returned the right amount.');
$this->assertEquals($user_a->getAccountName(), $result[0]->find('xpath', '/td[2]/a')->getText(), 'Filter by username returned the right user.');
$this->drupalGet('admin/people', ['query' => ['user' => $user_a->getEmail()]]);
$result = $this->xpath('//table/tbody/tr');
$this->assertCount(1, $result, 'Filter by username returned the right amount.');
$this->assertEquals($user_a->getAccountName(), $result[0]->find('xpath', '/td[2]/a')->getText(), 'Filter by username returned the right user.');
// Filter the users by permission 'administer taxonomy'.
$this->drupalGet('admin/people', ['query' => ['permission' => 'administer taxonomy']]);
// Check if the correct users show up.
$this->assertSession()->elementNotExists('xpath', static::getLinkSelectorForUser($user_a));
$this->assertSession()->elementExists('xpath', static::getLinkSelectorForUser($user_b));
$this->assertSession()->elementExists('xpath', static::getLinkSelectorForUser($user_c));
// Filter the users by role. Grab the system-generated role name for User C.
$roles = $user_c->getRoles();
unset($roles[array_search(RoleInterface::AUTHENTICATED_ID, $roles)]);
$this->drupalGet('admin/people', ['query' => ['role' => reset($roles)]]);
// Check if the correct users show up when filtered by role.
$this->assertSession()->elementNotExists('xpath', static::getLinkSelectorForUser($user_a));
$this->assertSession()->elementNotExists('xpath', static::getLinkSelectorForUser($user_b));
$this->assertSession()->elementExists('xpath', static::getLinkSelectorForUser($user_c));
// Test blocking of a user.
$account = $user_storage->load($user_c->id());
$this->assertTrue($account->isActive(), 'User C not blocked');
$edit = [];
$edit['action'] = 'user_block_user_action';
$edit['user_bulk_form[4]'] = TRUE;
$config
->set('notify.status_blocked', TRUE)
->save();
$this->drupalGet('admin/people', [
// Sort the table by username so that we know reliably which user will be
// targeted with the blocking action.
'query' => ['order' => 'name', 'sort' => 'asc'],
]);
$this->submitForm($edit, 'Apply to selected items');
$site_name = $this->config('system.site')->get('name');
$this->assertMailString('body', 'Your account on ' . $site_name . ' has been blocked.', 1, 'Blocked message found in the mail sent to user C.');
$user_storage->resetCache([$user_c->id()]);
$account = $user_storage->load($user_c->id());
$this->assertTrue($account->isBlocked(), 'User C blocked');
// Test filtering on admin page for blocked users
$this->drupalGet('admin/people', ['query' => ['status' => 2]]);
$this->assertSession()->elementNotExists('xpath', static::getLinkSelectorForUser($user_a));
$this->assertSession()->elementNotExists('xpath', static::getLinkSelectorForUser($user_b));
$this->assertSession()->elementExists('xpath', static::getLinkSelectorForUser($user_c));
// Test unblocking of a user from /admin/people page and sending of activation mail
$edit_unblock = [];
$edit_unblock['action'] = 'user_unblock_user_action';
$edit_unblock['user_bulk_form[4]'] = TRUE;
$this->drupalGet('admin/people', [
// Sort the table by username so that we know reliably which user will be
// targeted with the blocking action.
'query' => ['order' => 'name', 'sort' => 'asc'],
]);
$this->submitForm($edit_unblock, 'Apply to selected items');
$user_storage->resetCache([$user_c->id()]);
$account = $user_storage->load($user_c->id());
$this->assertTrue($account->isActive(), 'User C unblocked');
$this->assertMail("to", $account->getEmail(), "Activation mail sent to user C");
// Test blocking and unblocking another user from /user/[uid]/edit form and sending of activation mail
$user_d = $this->drupalCreateUser([]);
$user_storage->resetCache([$user_d->id()]);
$account1 = $user_storage->load($user_d->id());
$this->drupalGet('user/' . $account1->id() . '/edit');
$this->submitForm(['status' => 0], 'Save');
$user_storage->resetCache([$user_d->id()]);
$account1 = $user_storage->load($user_d->id());
$this->assertTrue($account1->isBlocked(), 'User D blocked');
$this->drupalGet('user/' . $account1->id() . '/edit');
$this->submitForm(['status' => TRUE], 'Save');
$user_storage->resetCache([$user_d->id()]);
$account1 = $user_storage->load($user_d->id());
$this->assertTrue($account1->isActive(), 'User D unblocked');
$this->assertMail("to", $account1->getEmail(), "Activation mail sent to user D");
}
/**
* Tests the alternate notification email address for user mails.
*/
public function testNotificationEmailAddress(): void {
// Test that the Notification Email address field is on the config page.
$admin_user = $this->drupalCreateUser([
'administer users',
'administer account settings',
]);
$this->drupalLogin($admin_user);
$this->drupalGet('admin/config/people/accounts');
$this->assertSession()->responseContains('id="edit-mail-notification-address"');
$this->drupalLogout();
// Test custom user registration approval email address(es).
$config = $this->config('user.settings');
// Allow users to register with admin approval.
$config
->set('verify_mail', TRUE)
->set('register', UserInterface::REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL)
->save();
// Set the site and notification email addresses.
$system = $this->config('system.site');
$server_address = $this->randomMachineName() . '@example.com';
$notify_address = $this->randomMachineName() . '@example.com';
$system
->set('mail', $server_address)
->set('mail_notification', $notify_address)
->save();
// Register a new user account.
$edit = [];
$edit['name'] = $this->randomMachineName();
$edit['mail'] = $edit['name'] . '@example.com';
$this->drupalGet('user/register');
$this->submitForm($edit, 'Create new account');
$subject = 'Account details for ' . $edit['name'] . ' at ' . $system->get('name') . ' (pending admin approval)';
// Ensure that admin notification mail is sent to the configured
// Notification Email address.
$admin_mail = $this->drupalGetMails([
'to' => $notify_address,
'from' => $server_address,
'subject' => $subject,
]);
$this->assertCount(1, $admin_mail, 'New user mail to admin is sent to configured Notification Email address');
// Ensure that user notification mail is sent from the configured
// Notification Email address.
$user_mail = $this->drupalGetMails([
'to' => $edit['mail'],
'from' => $server_address,
'reply-to' => $notify_address,
'subject' => $subject,
]);
$this->assertCount(1, $user_mail, 'New user mail to user is sent from configured Notification Email address');
}
}

View File

@@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\Core\Url;
use Drupal\dynamic_page_cache\EventSubscriber\DynamicPageCacheSubscriber;
use Drupal\Tests\BrowserTestBase;
/**
* Tests user blocks.
*
* @group user
*/
class UserBlocksTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['block', 'views'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A user with the 'administer blocks' permission.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->adminUser = $this->drupalCreateUser(['administer blocks']);
$this->drupalLogin($this->adminUser);
$this->drupalPlaceBlock('user_login_block', ['id' => 'user_blocks_test_user_login_block']);
$this->drupalLogout();
}
/**
* Tests that user login block is hidden from user/login.
*/
public function testUserLoginBlockVisibility(): void {
// Array keyed list where key being the URL address and value being expected
// visibility as boolean type.
$paths = [
'node' => TRUE,
'user/login' => FALSE,
'user/register' => TRUE,
'user/password' => TRUE,
];
foreach ($paths as $path => $expected_visibility) {
$this->drupalGet($path);
if ($expected_visibility) {
$this->assertSession()->elementExists('xpath', '//div[@id="block-user-blocks-test-user-login-block" and @role="form"]');
}
else {
$this->assertSession()->elementNotExists('xpath', '//div[@id="block-user-blocks-test-user-login-block" and @role="form"]');
}
}
}
/**
* Tests the user login block.
*/
public function testUserLoginBlock(): void {
// Create a user with some permission that anonymous users lack.
$user = $this->drupalCreateUser(['administer permissions']);
// Log in using the block.
$edit = [];
$edit['name'] = $user->getAccountName();
$edit['pass'] = $user->passRaw;
$this->drupalGet('admin/people/permissions');
$this->submitForm($edit, 'Log in');
$this->assertSession()->pageTextNotContains('User login');
// Check that we are still on the same page.
$this->assertSession()->addressEquals(Url::fromRoute('user.admin_permissions'));
// Now, log out and repeat with a non-403 page.
$this->drupalLogout();
$this->drupalGet('filter/tips');
$this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'MISS');
$this->submitForm($edit, 'Log in');
$this->assertSession()->pageTextNotContains('User login');
// Verify that we are still on the same page after login for allowed page.
$this->assertSession()->responseMatches('!<title.*?Compose tips.*?</title>!');
// Log out again and repeat with a non-403 page including query arguments.
$this->drupalLogout();
// @todo This test should not check for cache hits. Because it does and the
// cache has some clever redirect logic internally, we need to request the
// page twice to see the cache HIT in the headers.
// @see https://www.drupal.org/project/drupal/issues/2551419 #154
$this->drupalGet('filter/tips', ['query' => ['cat' => 'dog']]);
$this->drupalGet('filter/tips', ['query' => ['foo' => 'bar']]);
$this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'HIT');
$this->submitForm($edit, 'Log in');
$this->assertSession()->pageTextNotContains('User login');
// Verify that we are still on the same page after login for allowed page.
$this->assertSession()->responseMatches('!<title.*?Compose tips.*?</title>!');
$this->assertStringContainsString('/filter/tips?foo=bar', $this->getUrl(), 'Correct query arguments are displayed after login');
// Repeat with different query arguments.
$this->drupalLogout();
$this->drupalGet('filter/tips', ['query' => ['foo' => 'baz']]);
$this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'HIT');
$this->submitForm($edit, 'Log in');
$this->assertSession()->pageTextNotContains('User login');
// Verify that we are still on the same page after login for allowed page.
$this->assertSession()->responseMatches('!<title.*?Compose tips.*?</title>!');
$this->assertStringContainsString('/filter/tips?foo=baz', $this->getUrl(), 'Correct query arguments are displayed after login');
// Check that the user login block is not vulnerable to information
// disclosure to third party sites.
$this->drupalLogout();
$this->drupalGet('http://example.com/', ['external' => FALSE]);
$this->submitForm($edit, 'Log in');
// Check that we remain on the site after login.
$this->assertSession()->addressEquals($user->toUrl('canonical'));
// Verify that form validation errors are displayed immediately for forms
// in blocks and not on subsequent page requests.
$this->drupalLogout();
$edit = [];
$edit['name'] = 'foo';
$edit['pass'] = 'invalid password';
$this->drupalGet('filter/tips');
$this->submitForm($edit, 'Log in');
$this->assertSession()->pageTextContains('Unrecognized username or password. Forgot your password?');
$this->drupalGet('filter/tips');
$this->assertSession()->pageTextNotContains('Unrecognized username or password. Forgot your password?');
}
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\Tests\system\Functional\Entity\EntityWithUriCacheTagsTestBase;
use Drupal\user\Entity\Role;
use Drupal\user\Entity\User;
use Drupal\user\RoleInterface;
/**
* Tests the User entity's cache tags.
*
* @group user
*/
class UserCacheTagsTest extends EntityWithUriCacheTagsTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['user'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Give anonymous users permission to view user profiles, so that we can
// verify the cache tags of cached versions of user profile pages.
$user_role = Role::load(RoleInterface::ANONYMOUS_ID);
$user_role->grantPermission('access user profiles');
$user_role->save();
}
/**
* {@inheritdoc}
*/
protected function createEntity() {
// Create a "Llama" user.
$user = User::create([
'name' => 'Llama',
'status' => TRUE,
]);
$user->save();
return $user;
}
/**
* {@inheritdoc}
*/
protected function getAdditionalCacheTagsForEntityListing() {
return ['user:0', 'user:1'];
}
}

View File

@@ -0,0 +1,741 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\comment\CommentInterface;
use Drupal\comment\Entity\Comment;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\BrowserTestBase;
use Drupal\user\Entity\User;
/**
* Ensure that account cancellation methods work as expected.
*
* @group user
*/
class UserCancelTest extends BrowserTestBase {
use CommentTestTrait;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['node', 'comment'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
}
/**
* Attempt to cancel account without permission.
*/
public function testUserCancelWithoutPermission(): void {
$node_storage = $this->container->get('entity_type.manager')->getStorage('node');
$this->config('user.settings')->set('cancel_method', 'user_cancel_reassign')->save();
$user_storage = $this->container->get('entity_type.manager')->getStorage('user');
// Create a user.
$account = $this->drupalCreateUser([]);
$this->drupalLogin($account);
// Load a real user object.
$user_storage->resetCache([$account->id()]);
$account = $user_storage->load($account->id());
// Create a node.
$node = $this->drupalCreateNode(['uid' => $account->id()]);
// Attempt to cancel account.
$this->drupalGet('user/' . $account->id() . '/edit');
$this->assertSession()->pageTextNotContains("Cancel account");
// Attempt bogus account cancellation request confirmation.
$timestamp = $account->getLastLoginTime();
$this->drupalGet("user/" . $account->id() . "/cancel/confirm/$timestamp/" . user_pass_rehash($account, $timestamp));
$this->assertSession()->statusCodeEquals(403);
$user_storage->resetCache([$account->id()]);
$account = $user_storage->load($account->id());
$this->assertTrue($account->isActive(), 'User account was not canceled.');
// Confirm user's content has not been altered.
$node_storage->resetCache([$node->id()]);
$test_node = $node_storage->load($node->id());
$this->assertEquals($account->id(), $test_node->getOwnerId(), 'Node of the user has not been altered.');
$this->assertTrue($test_node->isPublished());
}
/**
* Tests ability to change the permission for canceling users.
*/
public function testUserCancelChangePermission(): void {
\Drupal::service('module_installer')->install(['user_form_test']);
$this->config('user.settings')->set('cancel_method', 'user_cancel_reassign')->save();
// Create a regular user.
$account = $this->drupalCreateUser([]);
$admin_user = $this->drupalCreateUser(['cancel other accounts']);
$this->drupalLogin($admin_user);
// Delete regular user.
$this->drupalGet('user_form_test_cancel/' . $account->id());
$this->submitForm([], 'Confirm');
// Confirm deletion.
$this->assertSession()->pageTextContains("Account {$account->getAccountName()} has been deleted.");
$this->assertNull(User::load($account->id()), 'User is not found in the database.');
}
/**
* Tests that user account for uid 1 cannot be cancelled.
*
* This should never be possible, or the site owner would become unable to
* administer the site.
*/
public function testUserCancelUid1(): void {
$user_storage = $this->container->get('entity_type.manager')->getStorage('user');
\Drupal::service('module_installer')->install(['views']);
// Try to cancel uid 1's account with a different user.
$admin_user = $this->drupalCreateUser(['administer users']);
$this->drupalLogin($admin_user);
$edit = [
'action' => 'user_cancel_user_action',
'user_bulk_form[0]' => TRUE,
];
$this->drupalGet('admin/people');
$this->submitForm($edit, 'Apply to selected items');
// Verify that uid 1's account was not cancelled.
$user_storage->resetCache([1]);
$user1 = $user_storage->load(1);
$this->assertTrue($user1->isActive(), 'User #1 still exists and is not blocked.');
}
/**
* Attempt invalid account cancellations.
*/
public function testUserCancelInvalid(): void {
$node_storage = $this->container->get('entity_type.manager')->getStorage('node');
$this->config('user.settings')->set('cancel_method', 'user_cancel_reassign')->save();
$user_storage = $this->container->get('entity_type.manager')->getStorage('user');
// Create a user.
$account = $this->drupalCreateUser(['cancel account']);
$this->drupalLogin($account);
// Load a real user object.
$user_storage->resetCache([$account->id()]);
$account = $user_storage->load($account->id());
// Create a node.
$node = $this->drupalCreateNode(['uid' => $account->id()]);
// Attempt to cancel account.
$this->drupalGet('user/' . $account->id() . '/cancel');
$timestamp = time();
$this->submitForm([], 'Confirm');
$this->assertSession()->pageTextContains('A confirmation request to cancel your account has been sent to your email address.');
// Attempt bogus account cancellation request confirmation.
$bogus_timestamp = $timestamp + 60;
$this->drupalGet("user/" . $account->id() . "/cancel/confirm/$bogus_timestamp/" . user_pass_rehash($account, $bogus_timestamp));
$this->assertSession()->pageTextContains('You have tried to use an account cancellation link that has expired. Request a new one using the form below.');
$user_storage->resetCache([$account->id()]);
$account = $user_storage->load($account->id());
$this->assertTrue($account->isActive(), 'User account was not canceled.');
// Attempt expired account cancellation request confirmation.
$bogus_timestamp = $timestamp - 86400 - 60;
$this->drupalGet("user/" . $account->id() . "/cancel/confirm/$bogus_timestamp/" . user_pass_rehash($account, $bogus_timestamp));
$this->assertSession()->pageTextContains('You have tried to use an account cancellation link that has expired. Request a new one using the form below.');
$user_storage->resetCache([$account->id()]);
$account = $user_storage->load($account->id());
$this->assertTrue($account->isActive(), 'User account was not canceled.');
// Confirm user's content has not been altered.
$node_storage->resetCache([$node->id()]);
$test_node = $node_storage->load($node->id());
$this->assertEquals($account->id(), $test_node->getOwnerId(), 'Node of the user has not been altered.');
$this->assertTrue($test_node->isPublished());
}
/**
* Disable account and keep all content.
*/
public function testUserBlock(): void {
$this->config('user.settings')->set('cancel_method', 'user_cancel_block')->save();
$user_storage = $this->container->get('entity_type.manager')->getStorage('user');
// Create a user.
$web_user = $this->drupalCreateUser(['cancel account']);
$this->drupalLogin($web_user);
// Load a real user object.
$user_storage->resetCache([$web_user->id()]);
$account = $user_storage->load($web_user->id());
// Attempt to cancel account.
$this->drupalGet('user/' . $account->id() . '/cancel');
$this->assertSession()->pageTextContains('Are you sure you want to cancel your account?');
$this->assertSession()->pageTextContains('Your account will be blocked and you will no longer be able to log in. All of your content will remain attributed to your username.');
$this->assertSession()->pageTextNotContains('Cancellation method');
// Confirm account cancellation.
$timestamp = time();
$this->submitForm([], 'Confirm');
$this->assertSession()->pageTextContains('A confirmation request to cancel your account has been sent to your email address.');
// Confirm account cancellation request.
$this->drupalGet("user/" . $account->id() . "/cancel/confirm/$timestamp/" . user_pass_rehash($account, $timestamp));
$user_storage->resetCache([$account->id()]);
$account = $user_storage->load($account->id());
$this->assertTrue($account->isBlocked(), 'User has been blocked.');
// Confirm that the confirmation message made it through to the end user.
$this->assertSession()->pageTextContains("Account {$account->getAccountName()} has been disabled.");
}
/**
* Disable account and unpublish all content.
*/
public function testUserBlockUnpublish(): void {
$node_storage = $this->container->get('entity_type.manager')->getStorage('node');
$this->config('user.settings')->set('cancel_method', 'user_cancel_block_unpublish')->save();
// Create comment field on page.
$this->addDefaultCommentField('node', 'page');
$user_storage = $this->container->get('entity_type.manager')->getStorage('user');
// Create a user.
$account = $this->drupalCreateUser(['cancel account']);
$this->drupalLogin($account);
// Load a real user object.
$user_storage->resetCache([$account->id()]);
$account = $user_storage->load($account->id());
// Create a node with two revisions.
$node = $this->drupalCreateNode(['uid' => $account->id()]);
$settings = get_object_vars($node);
$settings['revision'] = 1;
$node = $this->drupalCreateNode($settings);
// Add a comment to the page.
$comment_subject = $this->randomMachineName(8);
$comment_body = $this->randomMachineName(8);
$comment = Comment::create([
'subject' => $comment_subject,
'comment_body' => $comment_body,
'entity_id' => $node->id(),
'entity_type' => 'node',
'field_name' => 'comment',
'status' => CommentInterface::PUBLISHED,
'uid' => $account->id(),
]);
$comment->save();
// Attempt to cancel account.
$this->drupalGet('user/' . $account->id() . '/cancel');
$this->assertSession()->pageTextContains('Are you sure you want to cancel your account?');
$this->assertSession()->pageTextContains('Your account will be blocked and you will no longer be able to log in. All of your content will be hidden from everyone but administrators.');
// Confirm account cancellation.
$timestamp = time();
$this->submitForm([], 'Confirm');
$this->assertSession()->pageTextContains('A confirmation request to cancel your account has been sent to your email address.');
// Confirm account cancellation request.
$this->drupalGet("user/" . $account->id() . "/cancel/confirm/$timestamp/" . user_pass_rehash($account, $timestamp));
// Confirm that the user was redirected to the front page.
$this->assertSession()->addressEquals('');
$this->assertSession()->statusCodeEquals(200);
// Confirm that the confirmation message made it through to the end user.
$this->assertSession()->pageTextContains("Account {$account->getAccountName()} has been disabled.");
$user_storage->resetCache([$account->id()]);
$account = $user_storage->load($account->id());
$this->assertTrue($account->isBlocked(), 'User has been blocked.');
// Confirm user's content has been unpublished.
$node_storage->resetCache([$node->id()]);
$test_node = $node_storage->load($node->id());
$this->assertFalse($test_node->isPublished(), 'Node of the user has been unpublished.');
$test_node = $node_storage->loadRevision($node->getRevisionId());
$this->assertFalse($test_node->isPublished(), 'Node revision of the user has been unpublished.');
$storage = \Drupal::entityTypeManager()->getStorage('comment');
$storage->resetCache([$comment->id()]);
$comment = $storage->load($comment->id());
$this->assertFalse($comment->isPublished(), 'Comment of the user has been unpublished.');
}
/**
* Tests nodes are unpublished even if inaccessible to cancelling user.
*/
public function testUserBlockUnpublishNodeAccess(): void {
\Drupal::service('module_installer')->install(['node_access_test', 'user_form_test']);
// Setup node access
node_access_rebuild();
node_access_test_add_field(NodeType::load('page'));
\Drupal::state()->set('node_access_test.private', TRUE);
$this->config('user.settings')->set('cancel_method', 'user_cancel_block_unpublish')->save();
// Create a user.
$user_storage = $this->container->get('entity_type.manager')->getStorage('user');
$account = $this->drupalCreateUser(['cancel account']);
// Load a real user object.
$user_storage->resetCache([$account->id()]);
$account = $user_storage->load($account->id());
// Create a published private node.
$node = $this->drupalCreateNode([
'uid' => $account->id(),
'type' => 'page',
'status' => 1,
'private' => TRUE,
]);
// Cancel node author.
$admin_user = $this->drupalCreateUser(['cancel other accounts']);
$this->drupalLogin($admin_user);
$this->drupalGet('user_form_test_cancel/' . $account->id());
$this->submitForm([], 'Confirm');
// Confirm node has been unpublished, even though the admin user
// does not have permission to access it.
$node_storage = $this->container->get('entity_type.manager')->getStorage('node');
$node_storage->resetCache([$node->id()]);
$test_node = $node_storage->load($node->id());
$this->assertFalse($test_node->isPublished(), 'Node of the user has been unpublished.');
}
/**
* Delete account and anonymize all content.
*/
public function testUserAnonymize(): void {
$node_storage = $this->container->get('entity_type.manager')->getStorage('node');
$this->config('user.settings')->set('cancel_method', 'user_cancel_reassign')->save();
// Create comment field on page.
$this->addDefaultCommentField('node', 'page');
$user_storage = $this->container->get('entity_type.manager')->getStorage('user');
// Create a user.
$account = $this->drupalCreateUser(['cancel account']);
$this->drupalLogin($account);
// Load a real user object.
$user_storage->resetCache([$account->id()]);
$account = $user_storage->load($account->id());
// Create a simple node.
$node = $this->drupalCreateNode(['uid' => $account->id()]);
// Add a comment to the page.
$comment_subject = $this->randomMachineName(8);
$comment_body = $this->randomMachineName(8);
$comment = Comment::create([
'subject' => $comment_subject,
'comment_body' => $comment_body,
'entity_id' => $node->id(),
'entity_type' => 'node',
'field_name' => 'comment',
'status' => CommentInterface::PUBLISHED,
'uid' => $account->id(),
]);
$comment->save();
// Create a node with two revisions, the initial one belonging to the
// cancelling user.
$revision_node = $this->drupalCreateNode(['uid' => $account->id()]);
$revision = $revision_node->getRevisionId();
$settings = get_object_vars($revision_node);
$settings['revision'] = 1;
// Set new/current revision to someone else.
$settings['uid'] = 1;
$revision_node = $this->drupalCreateNode($settings);
// Attempt to cancel account.
$this->drupalGet('user/' . $account->id() . '/cancel');
$this->assertSession()->pageTextContains('Are you sure you want to cancel your account?');
$this->assertSession()->pageTextContains("Your account will be removed and all account information deleted. All of your content will be assigned to the {$this->config('user.settings')->get('anonymous')} user.");
// Confirm account cancellation.
$timestamp = time();
$this->submitForm([], 'Confirm');
$this->assertSession()->pageTextContains('A confirmation request to cancel your account has been sent to your email address.');
// Confirm account cancellation request.
$this->drupalGet("user/" . $account->id() . "/cancel/confirm/$timestamp/" . user_pass_rehash($account, $timestamp));
$user_storage->resetCache([$account->id()]);
$this->assertNull($user_storage->load($account->id()), 'User is not found in the database.');
// Confirm that user's content has been attributed to anonymous user.
$anonymous_user = User::getAnonymousUser();
$node_storage->resetCache([$node->id()]);
$test_node = $node_storage->load($node->id());
$this->assertEquals(0, $test_node->getOwnerId(), 'Node of the user has been attributed to anonymous user.');
$this->assertTrue($test_node->isPublished());
$test_node = $node_storage->loadRevision($revision);
$this->assertEquals(0, $test_node->getRevisionUser()->id(), 'Node revision of the user has been attributed to anonymous user.');
$this->assertTrue($test_node->isPublished());
$node_storage->resetCache([$revision_node->id()]);
$test_node = $node_storage->load($revision_node->id());
$this->assertNotEquals(0, $test_node->getOwnerId(), "Current revision of the user's node was not attributed to anonymous user.");
$this->assertTrue($test_node->isPublished());
$storage = \Drupal::entityTypeManager()->getStorage('comment');
$storage->resetCache([$comment->id()]);
$test_comment = $storage->load($comment->id());
$this->assertEquals(0, $test_comment->getOwnerId(), 'Comment of the user has been attributed to anonymous user.');
$this->assertTrue($test_comment->isPublished());
$this->assertEquals($anonymous_user->getDisplayName(), $test_comment->getAuthorName(), 'Comment of the user has been attributed to anonymous user name.');
// Confirm that the confirmation message made it through to the end user.
$this->assertSession()->pageTextContains("Account {$account->getAccountName()} has been deleted.");
}
/**
* Delete account and anonymize all content using a batch process.
*/
public function testUserAnonymizeBatch(): void {
$node_storage = $this->container->get('entity_type.manager')->getStorage('node');
$this->config('user.settings')->set('cancel_method', 'user_cancel_reassign')->save();
$user_storage = $this->container->get('entity_type.manager')->getStorage('user');
// Create a user.
$account = $this->drupalCreateUser(['cancel account']);
$this->drupalLogin($account);
// Load a real user object.
$user_storage->resetCache([$account->id()]);
$account = $user_storage->load($account->id());
// Create 11 nodes in order to trigger batch processing in
// node_mass_update().
$nodes = [];
for ($i = 0; $i < 11; $i++) {
$node = $this->drupalCreateNode(['uid' => $account->id()]);
$nodes[$node->id()] = $node;
}
// Attempt to cancel account.
$this->drupalGet('user/' . $account->id() . '/cancel');
$this->assertSession()->pageTextContains('Are you sure you want to cancel your account?');
$this->assertSession()->pageTextContains("Your account will be removed and all account information deleted. All of your content will be assigned to the {$this->config('user.settings')->get('anonymous')} user.");
// Confirm account cancellation.
$timestamp = time();
$this->submitForm([], 'Confirm');
$this->assertSession()->pageTextContains('A confirmation request to cancel your account has been sent to your email address.');
// Confirm account cancellation request.
$this->drupalGet("user/" . $account->id() . "/cancel/confirm/$timestamp/" . user_pass_rehash($account, $timestamp));
$user_storage->resetCache([$account->id()]);
$this->assertNull($user_storage->load($account->id()), 'User is not found in the database.');
// Confirm that user's content has been attributed to anonymous user.
$node_storage->resetCache(array_keys($nodes));
$test_nodes = $node_storage->loadMultiple(array_keys($nodes));
foreach ($test_nodes as $test_node) {
$this->assertEquals(0, $test_node->getOwnerId(), 'Node ' . $test_node->id() . ' of the user has been attributed to anonymous user.');
$this->assertTrue($test_node->isPublished());
}
}
/**
* Delete account and remove all content.
*/
public function testUserDelete(): void {
$node_storage = $this->container->get('entity_type.manager')->getStorage('node');
$this->config('user.settings')->set('cancel_method', 'user_cancel_delete')->save();
\Drupal::service('module_installer')->install(['comment']);
$this->resetAll();
$this->addDefaultCommentField('node', 'page');
$user_storage = $this->container->get('entity_type.manager')->getStorage('user');
// Create a user.
$account = $this->drupalCreateUser([
'cancel account',
'post comments',
'skip comment approval',
]);
$this->drupalLogin($account);
// Load a real user object.
$user_storage->resetCache([$account->id()]);
$account = $user_storage->load($account->id());
// Create a simple node.
$node = $this->drupalCreateNode(['uid' => $account->id()]);
// Create comment.
$edit = [];
$edit['subject[0][value]'] = $this->randomMachineName(8);
$edit['comment_body[0][value]'] = $this->randomMachineName(16);
$this->drupalGet('comment/reply/node/' . $node->id() . '/comment');
$this->submitForm($edit, 'Preview');
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains('Your comment has been posted.');
$comments = \Drupal::entityTypeManager()->getStorage('comment')->loadByProperties(['subject' => $edit['subject[0][value]']]);
$comment = reset($comments);
$this->assertNotEmpty($comment->id(), 'Comment found.');
// Create a node with two revisions, the initial one belonging to the
// cancelling user.
$revision_node = $this->drupalCreateNode(['uid' => $account->id()]);
$revision = $revision_node->getRevisionId();
$settings = get_object_vars($revision_node);
$settings['revision'] = 1;
// Set new/current revision to someone else.
$settings['uid'] = 1;
$revision_node = $this->drupalCreateNode($settings);
// Attempt to cancel account.
$this->drupalGet('user/' . $account->id() . '/cancel');
$this->assertSession()->pageTextContains('Are you sure you want to cancel your account?');
$this->assertSession()->pageTextContains('Your account will be removed and all account information deleted. All of your content will also be deleted.');
// Confirm account cancellation.
$timestamp = time();
$this->submitForm([], 'Confirm');
$this->assertSession()->pageTextContains('A confirmation request to cancel your account has been sent to your email address.');
// Confirm account cancellation request.
$this->drupalGet("user/" . $account->id() . "/cancel/confirm/$timestamp/" . user_pass_rehash($account, $timestamp));
$user_storage->resetCache([$account->id()]);
$this->assertNull($user_storage->load($account->id()), 'User is not found in the database.');
// Confirm there's only one session in the database. The user will be logged
// out and their session migrated.
// @see _user_cancel_session_regenerate()
$this->assertSame(1, (int) \Drupal::database()->select('sessions', 's')->countQuery()->execute()->fetchField());
// Confirm that user's content has been deleted.
$node_storage->resetCache([$node->id()]);
$this->assertNull($node_storage->load($node->id()), 'Node of the user has been deleted.');
$this->assertNull($node_storage->loadRevision($revision), 'Node revision of the user has been deleted.');
$node_storage->resetCache([$revision_node->id()]);
$this->assertInstanceOf(Node::class, $node_storage->load($revision_node->id()));
\Drupal::entityTypeManager()->getStorage('comment')->resetCache([$comment->id()]);
$this->assertNull(Comment::load($comment->id()), 'Comment of the user has been deleted.');
// Confirm that the confirmation message made it through to the end user.
$this->assertSession()->pageTextContains("Account {$account->getAccountName()} has been deleted.");
}
/**
* Create an administrative user and delete another user.
*/
public function testUserCancelByAdmin(): void {
$this->config('user.settings')->set('cancel_method', 'user_cancel_reassign')->save();
// Create a regular user.
$account = $this->drupalCreateUser([]);
// Create administrative user.
$admin_user = $this->drupalCreateUser(['administer users']);
$this->drupalLogin($admin_user);
// Delete regular user.
$this->drupalGet('user/' . $account->id() . '/cancel');
$this->assertSession()->pageTextContains("Are you sure you want to cancel the account {$account->getAccountName()}?");
$this->assertSession()->pageTextContains('Cancellation method');
// Confirm deletion.
$this->submitForm([], 'Confirm');
$this->assertSession()->pageTextContains("Account {$account->getAccountName()} has been deleted.");
$this->assertNull(User::load($account->id()), 'User is not found in the database.');
}
/**
* Tests deletion of a user account without an email address.
*/
public function testUserWithoutEmailCancelByAdmin(): void {
$this->config('user.settings')->set('cancel_method', 'user_cancel_reassign')->save();
// Create a regular user.
$account = $this->drupalCreateUser([]);
// This user has no email address.
$account->mail = '';
$account->save();
// Create administrative user.
$admin_user = $this->drupalCreateUser(['administer users']);
$this->drupalLogin($admin_user);
// Delete regular user without email address.
$this->drupalGet('user/' . $account->id() . '/cancel');
$this->assertSession()->pageTextContains("Are you sure you want to cancel the account {$account->getAccountName()}?");
$this->assertSession()->pageTextContains('Cancellation method');
// Confirm deletion.
$this->submitForm([], 'Confirm');
$this->assertSession()->pageTextContains("Account {$account->getAccountName()} has been deleted.");
$this->assertNull(User::load($account->id()), 'User is not found in the database.');
}
/**
* Create an administrative user and mass-delete other users.
*/
public function testMassUserCancelByAdmin(): void {
\Drupal::service('module_installer')->install(['views']);
$this->config('user.settings')->set('cancel_method', 'user_cancel_reassign')->save();
$user_storage = $this->container->get('entity_type.manager')->getStorage('user');
// Enable account cancellation notification.
$this->config('user.settings')->set('notify.status_canceled', TRUE)->save();
// Create administrative user.
$admin_user = $this->drupalCreateUser(['administer users']);
$this->drupalLogin($admin_user);
// Create some users.
$users = [];
for ($i = 0; $i < 3; $i++) {
$account = $this->drupalCreateUser([]);
$users[$account->id()] = $account;
}
// Cancel user accounts, including own one.
$edit = [];
$edit['action'] = 'user_cancel_user_action';
for ($i = 0; $i <= 4; $i++) {
$edit['user_bulk_form[' . $i . ']'] = TRUE;
}
$this->drupalGet('admin/people');
$this->submitForm($edit, 'Apply to selected items');
$this->assertSession()->pageTextContains('Are you sure you want to cancel these user accounts?');
$this->assertSession()->pageTextContains('Cancellation method');
$this->assertSession()->pageTextContains('Require email confirmation');
$this->assertSession()->pageTextContains('Notify user when account is canceled');
// Confirm deletion.
$this->submitForm([], 'Confirm');
$status = TRUE;
foreach ($users as $account) {
$status = $status && (str_contains($this->getTextContent(), "Account {$account->getAccountName()} has been deleted."));
$user_storage->resetCache([$account->id()]);
$status = $status && !$user_storage->load($account->id());
}
$this->assertTrue($status, 'Users deleted and not found in the database.');
// Ensure that admin account was not cancelled.
$this->assertSession()->pageTextContains('A confirmation request to cancel your account has been sent to your email address.');
$admin_user = $user_storage->load($admin_user->id());
$this->assertTrue($admin_user->isActive(), 'Administrative user is found in the database and enabled.');
// Verify that uid 1's account was not cancelled.
$user_storage->resetCache([1]);
$user1 = $user_storage->load(1);
$this->assertTrue($user1->isActive(), 'User #1 still exists and is not blocked.');
}
/**
* Tests user cancel with node access.
*/
public function testUserDeleteWithContentAndNodeAccess(): void {
\Drupal::service('module_installer')->install(['node_access_test']);
// Rebuild node access.
node_access_rebuild();
$account = $this->drupalCreateUser(['access content']);
$node = $this->drupalCreateNode(['type' => 'page', 'uid' => $account->id()]);
$account->delete();
$load2 = \Drupal::entityTypeManager()->getStorage('node')->load($node->id());
$this->assertEmpty($load2);
}
/**
* Delete account and anonymize all content and it's translations.
*/
public function testUserAnonymizeTranslations(): void {
$this->config('user.settings')->set('cancel_method', 'user_cancel_reassign')->save();
// Create comment field on page.
$this->addDefaultCommentField('node', 'page');
$user_storage = $this->container->get('entity_type.manager')->getStorage('user');
\Drupal::service('module_installer')->install([
'language',
'locale',
]);
\Drupal::service('router.builder')->rebuildIfNeeded();
ConfigurableLanguage::createFromLangcode('ur')->save();
// Rebuild the container to update the default language container variable.
$this->rebuildContainer();
$account = $this->drupalCreateUser(['cancel account']);
$this->drupalLogin($account);
$user_storage->resetCache([$account->id()]);
$account = $user_storage->load($account->id());
$node = $this->drupalCreateNode(['uid' => $account->id()]);
// Add a comment to the page.
$comment_subject = $this->randomMachineName(8);
$comment_body = $this->randomMachineName(8);
$comment = Comment::create([
'subject' => $comment_subject,
'comment_body' => $comment_body,
'entity_id' => $node->id(),
'entity_type' => 'node',
'field_name' => 'comment',
'status' => CommentInterface::PUBLISHED,
'uid' => $account->id(),
]);
$comment->save();
$comment->addTranslation('ur', [
'subject' => 'ur ' . $comment->label(),
'status' => CommentInterface::PUBLISHED,
])->save();
// Attempt to cancel account.
$this->drupalGet('user/' . $account->id() . '/cancel');
$this->assertSession()->pageTextContains('Are you sure you want to cancel your account?');
$this->assertSession()->pageTextContains('Your account will be removed and all account information deleted. All of your content will be assigned to the ' . $this->config('user.settings')->get('anonymous') . ' user.');
// Confirm account cancellation.
$timestamp = time();
$this->submitForm([], 'Confirm');
$this->assertSession()->pageTextContains('A confirmation request to cancel your account has been sent to your email address.');
// Confirm account cancellation request.
$this->drupalGet('user/' . $account->id() . "/cancel/confirm/$timestamp/" . user_pass_rehash($account, $timestamp));
$user_storage->resetCache([$account->id()]);
$this->assertNull($user_storage->load($account->id()), 'User is not found in the database.');
// Confirm that user's content has been attributed to anonymous user.
$anonymous_user = User::getAnonymousUser();
$storage = \Drupal::entityTypeManager()->getStorage('comment');
$storage->resetCache([$comment->id()]);
$test_comment = $storage->load($comment->id());
$this->assertEquals(0, $test_comment->getOwnerId());
$this->assertTrue($test_comment->isPublished(), 'Comment of the user has been attributed to anonymous user.');
$this->assertEquals($anonymous_user->getDisplayName(), $test_comment->getAuthorName());
$comment_translation = $test_comment->getTranslation('ur');
$this->assertEquals(0, $comment_translation->getOwnerId());
$this->assertTrue($comment_translation->isPublished(), 'Comment translation of the user has been attributed to anonymous user.');
$this->assertEquals($anonymous_user->getDisplayName(), $comment_translation->getAuthorName());
// Confirm that the confirmation message made it through to the end user.
$this->assertSession()->responseContains(t('%name has been deleted.', ['%name' => $account->getAccountName()]));
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the create user administration page.
*
* @group user
*/
class UserCreateFailMailTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['system_mail_failure_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests the create user administration page.
*/
public function testUserAdd(): void {
$user = $this->drupalCreateUser(['administer users']);
$this->drupalLogin($user);
// Replace the mail functionality with a fake, malfunctioning service.
$this->config('system.mail')->set('interface.default', 'test_php_mail_failure')->save();
// Create a user, but fail to send an email.
$name = $this->randomMachineName();
$edit = [
'name' => $name,
'mail' => $this->randomMachineName() . '@example.com',
'pass[pass1]' => $pass = $this->randomString(),
'pass[pass2]' => $pass,
'notify' => TRUE,
];
$this->drupalGet('admin/people/create');
$this->submitForm($edit, 'Create new account');
$this->assertSession()->pageTextContains('Unable to send email. Contact the site administrator if the problem persists.');
$this->assertSession()->pageTextNotContains('A welcome message with further instructions has been emailed to the new user ' . $edit['name'] . '.');
}
}

View File

@@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\Core\Test\AssertMailTrait;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the create user administration page.
*
* @group user
*/
class UserCreateTest extends BrowserTestBase {
use AssertMailTrait {
getMails as drupalGetMails;
}
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['image'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests user creation and display from the administration interface.
*/
public function testUserAdd(): void {
$user = $this->drupalCreateUser(['administer users']);
$this->drupalLogin($user);
$this->assertEquals(\Drupal::time()->getRequestTime(), $user->getCreatedTime(), 'Creating a user sets default "created" timestamp.');
$this->assertEquals(\Drupal::time()->getRequestTime(), $user->getChangedTime(), 'Creating a user sets default "changed" timestamp.');
// Create a field.
$field_name = 'test_field';
FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'user',
'module' => 'image',
'type' => 'image',
'cardinality' => 1,
'locked' => FALSE,
'indexes' => ['target_id' => ['target_id']],
'settings' => [
'uri_scheme' => 'public',
],
])->save();
FieldConfig::create([
'field_name' => $field_name,
'entity_type' => 'user',
'label' => 'Picture',
'bundle' => 'user',
'description' => 'Your virtual face or picture.',
'required' => FALSE,
'settings' => [
'file_extensions' => 'png gif jpg jpeg webp',
'file_directory' => 'pictures',
'max_filesize' => '30 KB',
'alt_field' => 0,
'title_field' => 0,
'max_resolution' => '85x85',
'min_resolution' => '',
],
])->save();
// Test user creation page for valid fields.
$this->drupalGet('admin/people/create');
$this->assertSession()->fieldValueEquals('edit-status-0', '1');
$this->assertSession()->fieldValueEquals('edit-status-1', '1');
$this->assertSession()->checkboxChecked('edit-status-1');
// Test that browser autocomplete behavior does not occur.
$this->assertSession()->responseNotContains('data-user-info-from-browser');
// Test that the password strength indicator displays.
$config = $this->config('user.settings');
$config->set('password_strength', TRUE)->save();
$this->drupalGet('admin/people/create');
$this->assertSession()->responseContains("Password strength:");
$config->set('password_strength', FALSE)->save();
$this->drupalGet('admin/people/create');
$this->assertSession()->responseNotContains("Password strength:");
// We create two users, notifying one and not notifying the other, to
// ensure that the tests work in both cases.
foreach ([FALSE, TRUE] as $notify) {
$name = $this->randomMachineName();
$edit = [
'name' => $name,
'mail' => $this->randomMachineName() . '@example.com',
'pass[pass1]' => $pass = $this->randomString(),
'pass[pass2]' => $pass,
'notify' => $notify,
];
$this->drupalGet('admin/people/create');
$this->submitForm($edit, 'Create new account');
if ($notify) {
$this->assertSession()->pageTextContains('A welcome message with further instructions has been emailed to the new user ' . $edit['name'] . '.');
$this->assertCount(1, $this->drupalGetMails(), 'Notification email sent');
}
else {
$this->assertSession()->pageTextContains('Created a new user account for ' . $edit['name'] . '. No email has been sent.');
$this->assertCount(0, $this->drupalGetMails(), 'Notification email not sent');
}
$this->drupalGet('admin/people');
$this->assertSession()->pageTextContains($edit['name']);
$user = user_load_by_name($name);
$this->assertTrue($user->isActive(), 'User is not blocked');
}
// Test that the password '0' is considered a password.
// @see https://www.drupal.org/node/2563751.
$name = $this->randomMachineName();
$edit = [
'name' => $name,
'mail' => $this->randomMachineName() . '@example.com',
'pass[pass1]' => 0,
'pass[pass2]' => 0,
'notify' => FALSE,
];
$this->drupalGet('admin/people/create');
$this->submitForm($edit, 'Create new account');
$this->assertSession()->pageTextContains("Created a new user account for $name. No email has been sent");
$this->assertSession()->pageTextNotContains('Password field is required');
}
}

View File

@@ -0,0 +1,262 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\Core\Cache\Cache;
use Drupal\Tests\BrowserTestBase;
/**
* Tests user edit page.
*
* @group user
*/
class UserEditTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests user edit page.
*/
public function testUserEdit(): void {
// Test user edit functionality.
$user1 = $this->drupalCreateUser(['change own username']);
$user2 = $this->drupalCreateUser([]);
$this->drupalLogin($user1);
// Test that error message appears when attempting to use a non-unique user name.
$edit['name'] = $user2->getAccountName();
$this->drupalGet("user/" . $user1->id() . "/edit");
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("The username {$edit['name']} is already taken.");
// Check that the default value in user name field
// is the raw value and not a formatted one.
\Drupal::state()->set('user_hooks_test_user_format_name_alter', TRUE);
\Drupal::service('module_installer')->install(['user_hooks_test']);
Cache::invalidateTags(['rendered']);
$this->drupalGet('user/' . $user1->id() . '/edit');
$this->assertSession()->fieldValueEquals('name', $user1->getAccountName());
// Ensure the formatted name is displayed when expected.
$this->drupalGet('user/' . $user1->id());
$this->assertSession()->responseContains($user1->getDisplayName());
$this->assertSession()->titleEquals(strip_tags($user1->getDisplayName()) . ' | Drupal');
// Check that filling out a single password field does not validate.
$edit = [];
$edit['pass[pass1]'] = '';
$edit['pass[pass2]'] = $this->randomMachineName();
$this->drupalGet("user/" . $user1->id() . "/edit");
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("The specified passwords do not match.");
$edit['pass[pass1]'] = $this->randomMachineName();
$edit['pass[pass2]'] = '';
$this->drupalGet("user/" . $user1->id() . "/edit");
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("The specified passwords do not match.");
// Test that the error message appears when attempting to change the mail or
// pass without the current password.
$edit = [];
$edit['mail'] = $this->randomMachineName() . '@new.example.com';
$this->drupalGet("user/" . $user1->id() . "/edit");
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("Your current password is missing or incorrect; it's required to change the Email.");
$edit['current_pass'] = $user1->passRaw;
$this->drupalGet("user/" . $user1->id() . "/edit");
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("The changes have been saved.");
// Test that the user must enter current password before changing passwords.
$edit = [];
$edit['pass[pass1]'] = $new_pass = $this->randomMachineName();
$edit['pass[pass2]'] = $new_pass;
$this->drupalGet("user/" . $user1->id() . "/edit");
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("Your current password is missing or incorrect; it's required to change the Password.");
// Try again with the current password.
$edit['current_pass'] = $user1->passRaw;
$this->drupalGet("user/" . $user1->id() . "/edit");
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("The changes have been saved.");
// Confirm there's only one session in the database as the existing session
// has been migrated when the password is changed.
// @see \Drupal\user\Entity\User::postSave()
$this->assertSame(1, (int) \Drupal::database()->select('sessions', 's')->countQuery()->execute()->fetchField());
// Make sure the changed timestamp is updated.
$this->assertEquals(\Drupal::time()->getRequestTime(), $user1->getChangedTime(), 'Changing a user sets "changed" timestamp.');
// Make sure the user can log in with their new password.
$this->drupalLogout();
$user1->passRaw = $new_pass;
$this->drupalLogin($user1);
$this->drupalLogout();
// Test that the password strength indicator displays.
$config = $this->config('user.settings');
$this->drupalLogin($user1);
$config->set('password_strength', TRUE)->save();
$this->drupalGet("user/" . $user1->id() . "/edit");
$this->submitForm($edit, 'Save');
$this->assertSession()->responseContains("Password strength:");
$config->set('password_strength', FALSE)->save();
$this->drupalGet("user/" . $user1->id() . "/edit");
$this->submitForm($edit, 'Save');
$this->assertSession()->responseNotContains("Password strength:");
// Check that the user status field has the correct value and that it is
// properly displayed.
$admin_user = $this->drupalCreateUser(['administer users']);
$this->drupalLogin($admin_user);
$this->drupalGet('user/' . $user1->id() . '/edit');
$this->assertSession()->checkboxNotChecked('edit-status-0');
$this->assertSession()->checkboxChecked('edit-status-1');
$edit = ['status' => 0];
$this->drupalGet('user/' . $user1->id() . '/edit');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('The changes have been saved.');
$this->assertSession()->checkboxChecked('edit-status-0');
$this->assertSession()->checkboxNotChecked('edit-status-1');
$edit = ['status' => 1];
$this->drupalGet('user/' . $user1->id() . '/edit');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('The changes have been saved.');
$this->assertSession()->checkboxNotChecked('edit-status-0');
$this->assertSession()->checkboxChecked('edit-status-1');
}
/**
* Tests setting the password to "0".
*
* We discovered in https://www.drupal.org/node/2563751 that logging in with a
* password that is literally "0" was not possible. This test ensures that
* this regression can't happen again.
*/
public function testUserWith0Password(): void {
$admin = $this->drupalCreateUser(['administer users']);
$this->drupalLogin($admin);
// Create a regular user.
$user1 = $this->drupalCreateUser([]);
$edit = ['pass[pass1]' => '0', 'pass[pass2]' => '0'];
$this->drupalGet("user/" . $user1->id() . "/edit");
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("The changes have been saved.");
}
/**
* Tests editing of a user account without an email address.
*/
public function testUserWithoutEmailEdit(): void {
// Test that an admin can edit users without an email address.
$admin = $this->drupalCreateUser(['administer users']);
$this->drupalLogin($admin);
// Create a regular user.
$user1 = $this->drupalCreateUser([]);
// This user has no email address.
$user1->mail = '';
$user1->save();
$this->drupalGet("user/" . $user1->id() . "/edit");
$this->submitForm(['mail' => ''], 'Save');
$this->assertSession()->pageTextContains("The changes have been saved.");
}
/**
* Tests well known change password route redirects to user edit form.
*/
public function testUserWellKnownChangePasswordAuth(): void {
$account = $this->drupalCreateUser([]);
$this->drupalLogin($account);
$this->drupalGet('.well-known/change-password');
$this->assertSession()->addressEquals("user/" . $account->id() . "/edit");
}
/**
* Tests well known change password route returns 403 to anonymous user.
*/
public function testUserWellKnownChangePasswordAnon(): void {
$this->drupalGet('.well-known/change-password');
$this->assertSession()->statusCodeEquals(403);
}
/**
* Tests that a user is able to change site language.
*/
public function testUserChangeSiteLanguage(): void {
// Install these modules here as these aren't needed for other test methods.
\Drupal::service('module_installer')->install([
'content_translation',
'language',
]);
// Create and login as an admin user to add a new language and enable
// translation for user accounts.
$adminUser = $this->drupalCreateUser([
'administer account settings',
'administer languages',
'administer content translation',
'administer users',
'translate any entity',
]);
$this->drupalLogin($adminUser);
// Add a new language into the system.
$edit = [
'predefined_langcode' => 'fr',
];
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm($edit, 'Add language');
$this->assertSession()->pageTextContains('French');
// Enable translation for user accounts.
$edit = [
'language[content_translation]' => 1,
];
$this->drupalGet('admin/config/people/accounts');
$this->submitForm($edit, 'Save configuration');
$this->assertSession()->pageTextContains('The configuration options have been saved.');
// Create a regular user for whom translation will be enabled.
$webUser = $this->drupalCreateUser();
// Create a translation for a regular user account.
$this->drupalGet('user/' . $webUser->id() . '/translations/add/en/fr');
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains('The changes have been saved.');
// Update the site language of the user account.
$edit = [
'preferred_langcode' => 'fr',
];
$this->drupalGet('user/' . $webUser->id() . '/edit');
$this->submitForm($edit, 'Save');
$this->assertSession()->statusCodeEquals(200);
}
/**
* Tests the account form implements entity field access for mail.
*/
public function testUserMailFieldAccess(): void {
\Drupal::state()->set('user_access_test_forbid_mail_edit', TRUE);
\Drupal::service('module_installer')->install(['user_access_test']);
$user = $this->drupalCreateUser();
$this->drupalLogin($user);
$this->drupalGet("user/" . $user->id() . "/edit");
$this->assertFalse($this->getSession()->getPage()->hasField('mail'));
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\user\UserInterface;
/**
* Tests user edited own account can still log in.
*
* @group user
*/
class UserEditedOwnAccountTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
public function testUserEditedOwnAccount(): void {
// Change account setting 'Who can register accounts?' to Administrators
// only.
$this->config('user.settings')->set('register', UserInterface::REGISTER_ADMINISTRATORS_ONLY)->save();
// Create a new user account and log in.
$account = $this->drupalCreateUser(['change own username']);
$this->drupalLogin($account);
// Change own username.
$edit = [];
$edit['name'] = $this->randomMachineName();
$this->drupalGet('user/' . $account->id() . '/edit');
$this->submitForm($edit, 'Save');
// Log out.
$this->drupalLogout();
// Set the new name on the user account and attempt to log back in.
$account->name = $edit['name'];
$this->drupalLogin($account);
}
}

View File

@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\BrowserTestBase;
/**
* Tests preferred language configuration and language selector access.
*
* @group user
*/
class UserLanguageCreationTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['user', 'language'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Functional test for language handling during user creation.
*/
public function testLocalUserCreation(): void {
// User to add and remove language and create new users.
$admin_user = $this->drupalCreateUser([
'administer languages',
'access administration pages',
'administer users',
]);
$this->drupalLogin($admin_user);
// Add predefined language.
$langcode = 'fr';
ConfigurableLanguage::createFromLangcode($langcode)->save();
// Set language negotiation.
$edit = [
'language_interface[enabled][language-url]' => TRUE,
];
$this->drupalGet('admin/config/regional/language/detection');
$this->submitForm($edit, 'Save settings');
$this->assertSession()->pageTextContains('Language detection configuration saved.');
// Check if the language selector is available on admin/people/create and
// set to the currently active language.
$this->drupalGet($langcode . '/admin/people/create');
$this->assertTrue($this->assertSession()->optionExists("edit-preferred-langcode", $langcode)->isSelected());
// Create a user with the admin/people/create form and check if the correct
// language is set.
$username = $this->randomMachineName(10);
$edit = [
'name' => $username,
'mail' => $this->randomMachineName(4) . '@example.com',
'pass[pass1]' => $username,
'pass[pass2]' => $username,
];
$this->drupalGet($langcode . '/admin/people/create');
$this->submitForm($edit, 'Create new account');
$user = user_load_by_name($username);
$this->assertEquals($langcode, $user->getPreferredLangcode(), 'New user has correct preferred language set.');
$this->assertEquals($langcode, $user->language()->getId(), 'New user has correct profile language set.');
// Register a new user and check if the language selector is hidden.
$this->drupalLogout();
$this->drupalGet($langcode . '/user/register');
$this->assertSession()->fieldNotExists('language[fr]');
$username = $this->randomMachineName(10);
$edit = [
'name' => $username,
'mail' => $this->randomMachineName(4) . '@example.com',
];
$this->drupalGet($langcode . '/user/register');
$this->submitForm($edit, 'Create new account');
$user = user_load_by_name($username);
$this->assertEquals($langcode, $user->getPreferredLangcode(), 'New user has correct preferred language set.');
$this->assertEquals($langcode, $user->language()->getId(), 'New user has correct profile language set.');
// Test that the admin can use the language selector and if the correct
// language is saved.
$user_edit = $langcode . '/user/' . $user->id() . '/edit';
$this->drupalLogin($admin_user);
$this->drupalGet($user_edit);
$this->assertTrue($this->assertSession()->optionExists("edit-preferred-langcode", $langcode)->isSelected());
// Set passRaw so we can log in the new user.
$user->passRaw = $this->randomMachineName(10);
$edit = [
'pass[pass1]' => $user->passRaw,
'pass[pass2]' => $user->passRaw,
];
$this->drupalGet($user_edit);
$this->submitForm($edit, 'Save');
$this->drupalLogin($user);
$this->drupalGet($user_edit);
$this->assertTrue($this->assertSession()->optionExists("edit-preferred-langcode", $langcode)->isSelected());
}
}

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Tests\BrowserTestBase;
/**
* Functional tests for a user's ability to change their default language.
*
* @group user
*/
class UserLanguageTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['user', 'language'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests if user can change their default language.
*/
public function testUserLanguageConfiguration(): void {
// User to add and remove language.
$admin_user = $this->drupalCreateUser([
'administer languages',
'access administration pages',
]);
// User to change their default language.
$web_user = $this->drupalCreateUser();
// Add custom language.
$this->drupalLogin($admin_user);
// Code for the language.
$langcode = 'xx';
// The English name for the language.
$name = $this->randomMachineName(16);
$edit = [
'predefined_langcode' => 'custom',
'langcode' => $langcode,
'label' => $name,
'direction' => LanguageInterface::DIRECTION_LTR,
];
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm($edit, 'Add custom language');
$this->drupalLogout();
// Log in as normal user and edit account settings.
$this->drupalLogin($web_user);
$path = 'user/' . $web_user->id() . '/edit';
$this->drupalGet($path);
// Ensure language settings widget is available.
$this->assertSession()->pageTextContains('Language');
// Ensure custom language is present.
$this->assertSession()->pageTextContains($name);
// Switch to our custom language.
$edit = [
'preferred_langcode' => $langcode,
];
$this->drupalGet($path);
$this->submitForm($edit, 'Save');
// Ensure form was submitted successfully.
$this->assertSession()->pageTextContains('The changes have been saved.');
// Check if language was changed.
$this->assertTrue($this->assertSession()->optionExists('edit-preferred-langcode', $langcode)->isSelected());
$this->drupalLogout();
}
}

View File

@@ -0,0 +1,613 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\Core\Flood\DatabaseBackend;
use Drupal\Core\Test\AssertMailTrait;
use Drupal\Core\Database\Database;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
use Drupal\user\Controller\UserAuthenticationController;
use GuzzleHttp\Cookie\CookieJar;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Serializer;
/**
* Tests login and password reset via direct HTTP.
*
* @group user
*/
class UserLoginHttpTest extends BrowserTestBase {
use AssertMailTrait {
getMails as drupalGetMails;
}
/**
* Modules to install.
*
* @var array
*/
protected static $modules = ['dblog'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The cookie jar.
*
* @var \GuzzleHttp\Cookie\CookieJar
*/
protected $cookies;
/**
* The serializer.
*
* @var \Symfony\Component\Serializer\Serializer
*/
protected $serializer;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->cookies = new CookieJar();
$encoders = [new JsonEncoder(), new XmlEncoder()];
$this->serializer = new Serializer([], $encoders);
}
/**
* Executes a login HTTP request for a given serialization format.
*
* @param string $name
* The username.
* @param string $pass
* The user password.
* @param string $format
* The format to use to make the request.
*
* @return \Psr\Http\Message\ResponseInterface
* The HTTP response.
*/
protected function loginRequest($name, $pass, $format = 'json') {
$user_login_url = Url::fromRoute('user.login.http')
->setRouteParameter('_format', $format)
->setAbsolute();
$request_body = [];
if (isset($name)) {
$request_body['name'] = $name;
}
if (isset($pass)) {
$request_body['pass'] = $pass;
}
$result = \Drupal::httpClient()->post($user_login_url->toString(), [
'body' => $this->serializer->encode($request_body, $format),
'headers' => [
'Accept' => "application/$format",
],
'http_errors' => FALSE,
'cookies' => $this->cookies,
]);
return $result;
}
/**
* Tests user session life cycle.
*/
public function testLogin(): void {
// Without the serialization module only JSON is supported.
$this->doTestLogin('json');
// Enable serialization so we have access to additional formats.
$this->container->get('module_installer')->install(['serialization']);
$this->rebuildAll();
$this->doTestLogin('json');
$this->doTestLogin('xml');
}
/**
* Do login testing for a given serialization format.
*
* @param string $format
* Serialization format.
*/
protected function doTestLogin($format) {
$client = \Drupal::httpClient();
// Create new user for each iteration to reset flood.
// Grant the user administer users permissions to they can see the
// 'roles' field.
$account = $this->drupalCreateUser(['administer users']);
$name = $account->getAccountName();
$pass = $account->passRaw;
$login_status_url = $this->getLoginStatusUrlString($format);
$response = $client->get($login_status_url);
$this->assertHttpResponse($response, 200, UserAuthenticationController::LOGGED_OUT);
// Flooded.
$this->config('user.flood')
->set('user_limit', 3)
->save();
$response = $this->loginRequest($name, 'wrong-pass', $format);
$this->assertHttpResponseWithMessage($response, 400, 'Sorry, unrecognized username or password.', $format);
$response = $this->loginRequest($name, 'wrong-pass', $format);
$this->assertHttpResponseWithMessage($response, 400, 'Sorry, unrecognized username or password.', $format);
$response = $this->loginRequest($name, 'wrong-pass', $format);
$this->assertHttpResponseWithMessage($response, 400, 'Sorry, unrecognized username or password.', $format);
$response = $this->loginRequest($name, 'wrong-pass', $format);
$this->assertHttpResponseWithMessage($response, 403, 'Too many failed login attempts from your IP address. This IP address is temporarily blocked.', $format);
// After testing the flood control we can increase the limit.
$this->config('user.flood')
->set('user_limit', 100)
->save();
$response = $this->loginRequest(NULL, NULL, $format);
$this->assertHttpResponseWithMessage($response, 400, 'Missing credentials.', $format);
$response = $this->loginRequest(NULL, $pass, $format);
$this->assertHttpResponseWithMessage($response, 400, 'Missing credentials.name.', $format);
$response = $this->loginRequest($name, NULL, $format);
$this->assertHttpResponseWithMessage($response, 400, 'Missing credentials.pass.', $format);
// Blocked.
$account
->block()
->save();
$response = $this->loginRequest($name, $pass, $format);
$this->assertHttpResponseWithMessage($response, 400, 'The user has not been activated or is blocked.', $format);
$account
->activate()
->save();
$response = $this->loginRequest($name, 'garbage', $format);
$this->assertHttpResponseWithMessage($response, 400, 'Sorry, unrecognized username or password.', $format);
$response = $this->loginRequest('garbage', $pass, $format);
$this->assertHttpResponseWithMessage($response, 400, 'Sorry, unrecognized username or password.', $format);
$response = $this->loginRequest($name, $pass, $format);
$this->assertEquals(200, $response->getStatusCode());
$result_data = $this->serializer->decode((string) $response->getBody(), $format);
$this->assertEquals($name, $result_data['current_user']['name']);
$this->assertEquals($account->id(), $result_data['current_user']['uid']);
$this->assertEquals($account->getRoles(), $result_data['current_user']['roles']);
$logout_token = $result_data['logout_token'];
// Logging in while already logged in results in a 403 with helpful message.
$response = $this->loginRequest($name, $pass, $format);
$this->assertSame(403, $response->getStatusCode());
$this->assertSame(['message' => 'This route can only be accessed by anonymous users.'], $this->serializer->decode((string) $response->getBody(), $format));
$response = $client->get($login_status_url, ['cookies' => $this->cookies]);
$this->assertHttpResponse($response, 200, UserAuthenticationController::LOGGED_IN);
$response = $this->logoutRequest($format, $logout_token);
$this->assertEquals(204, $response->getStatusCode());
$response = $client->get($login_status_url, ['cookies' => $this->cookies]);
$this->assertHttpResponse($response, 200, UserAuthenticationController::LOGGED_OUT);
$this->resetFlood();
}
/**
* Executes a password HTTP request for a given serialization format.
*
* @param array $request_body
* The request body.
* @param string $format
* The format to use to make the request.
*
* @return \Psr\Http\Message\ResponseInterface
* The HTTP response.
*/
protected function passwordRequest(array $request_body, $format = 'json') {
$password_reset_url = Url::fromRoute('user.pass.http')
->setRouteParameter('_format', $format)
->setAbsolute();
$result = \Drupal::httpClient()->post($password_reset_url->toString(), [
'body' => $this->serializer->encode($request_body, $format),
'headers' => [
'Accept' => "application/$format",
],
'http_errors' => FALSE,
'cookies' => $this->cookies,
]);
return $result;
}
/**
* Tests user password reset.
*/
public function testPasswordReset(): void {
// Create a user account.
$account = $this->drupalCreateUser();
// Without the serialization module only JSON is supported.
$this->doTestPasswordReset('json', $account);
// Enable serialization so we have access to additional formats.
$this->container->get('module_installer')->install(['serialization']);
$this->rebuildAll();
$this->doTestPasswordReset('json', $account);
$this->doTestPasswordReset('xml', $account);
$this->doTestGlobalLoginFloodControl('json');
$this->doTestPerUserLoginFloodControl('json');
$this->doTestLogoutCsrfProtection('json');
}
/**
* Gets a value for a given key from the response.
*
* @param \Psr\Http\Message\ResponseInterface $response
* The response object.
* @param string $key
* The key for the value.
* @param string $format
* The encoded format.
*
* @return mixed
* The value for the key.
*/
protected function getResultValue(ResponseInterface $response, $key, $format) {
$decoded = $this->serializer->decode((string) $response->getBody(), $format);
if (is_array($decoded)) {
return $decoded[$key];
}
else {
return $decoded->{$key};
}
}
/**
* Resets all flood entries.
*/
protected function resetFlood() {
$this->container->get('database')->delete(DatabaseBackend::TABLE_NAME)->execute();
}
/**
* Tests the global login flood control for a given serialization format.
*
* @param string $format
* The encoded format.
*
* @see \Drupal\basic_auth\Authentication\Provider\BasicAuthTest::testGlobalLoginFloodControl
* @see \Drupal\Tests\user\Functional\UserLoginTest::testGlobalLoginFloodControl
*/
public function doTestGlobalLoginFloodControl(string $format): void {
$database = \Drupal::database();
$this->config('user.flood')
->set('ip_limit', 2)
// Set a high per-user limit out so that it is not relevant in the test.
->set('user_limit', 4000)
->save();
$user = $this->drupalCreateUser([]);
$incorrect_user = clone $user;
$incorrect_user->passRaw .= 'incorrect';
// Try 2 failed logins.
for ($i = 0; $i < 2; $i++) {
$response = $this->loginRequest($incorrect_user->getAccountName(), $incorrect_user->passRaw, $format);
$this->assertEquals('400', $response->getStatusCode());
}
// IP limit has reached to its limit. Even valid user credentials will fail.
$response = $this->loginRequest($user->getAccountName(), $user->passRaw, $format);
$this->assertHttpResponseWithMessage($response, 403, 'Access is blocked because of IP based flood prevention.', $format);
$last_log = $database->select('watchdog', 'w')
->fields('w', ['message'])
->condition('type', 'user')
->orderBy('wid', 'DESC')
->range(0, 1)
->execute()
->fetchField();
$this->assertEquals('Flood control blocked login attempt from %ip', $last_log, 'A watchdog message was logged for the login attempt blocked by flood control per IP.');
}
/**
* Checks a response for status code and body.
*
* @param \Psr\Http\Message\ResponseInterface $response
* The response object.
* @param int $expected_code
* The expected status code.
* @param string $expected_body
* The expected response body.
*
* @internal
*/
protected function assertHttpResponse(ResponseInterface $response, int $expected_code, string $expected_body): void {
$this->assertEquals($expected_code, $response->getStatusCode());
$this->assertEquals($expected_body, $response->getBody());
}
/**
* Checks a response for status code and message.
*
* @param \Psr\Http\Message\ResponseInterface $response
* The response object.
* @param int $expected_code
* The expected status code.
* @param string $expected_message
* The expected message encoded in response.
* @param string $format
* The format that the response is encoded in.
*
* @internal
*/
protected function assertHttpResponseWithMessage(ResponseInterface $response, int $expected_code, string $expected_message, string $format = 'json'): void {
$this->assertEquals($expected_code, $response->getStatusCode());
$this->assertEquals($expected_message, $this->getResultValue($response, 'message', $format));
}
/**
* Tests the per-user login flood control for a given serialization format.
*
* @see \Drupal\basic_auth\Authentication\Provider\BasicAuthTest::testPerUserLoginFloodControl
* @see \Drupal\Tests\user\Functional\UserLoginTest::testPerUserLoginFloodControl
*/
public function doTestPerUserLoginFloodControl($format): void {
$database = \Drupal::database();
foreach ([TRUE, FALSE] as $uid_only_setting) {
$this->config('user.flood')
// Set a high global limit out so that it is not relevant in the test.
->set('ip_limit', 4000)
->set('user_limit', 3)
->set('uid_only', $uid_only_setting)
->save();
$user1 = $this->drupalCreateUser([]);
$incorrect_user1 = clone $user1;
$incorrect_user1->passRaw .= 'incorrect';
$user2 = $this->drupalCreateUser([]);
// Try 2 failed logins.
for ($i = 0; $i < 2; $i++) {
$response = $this->loginRequest($incorrect_user1->getAccountName(), $incorrect_user1->passRaw, $format);
$this->assertHttpResponseWithMessage($response, 400, 'Sorry, unrecognized username or password.', $format);
}
// A successful login will reset the per-user flood control count.
$response = $this->loginRequest($user1->getAccountName(), $user1->passRaw, $format);
$result_data = $this->serializer->decode((string) $response->getBody(), $format);
$this->logoutRequest($format, $result_data['logout_token']);
// Try 3 failed logins for user 1, they will not trigger flood control.
for ($i = 0; $i < 3; $i++) {
$response = $this->loginRequest($incorrect_user1->getAccountName(), $incorrect_user1->passRaw, $format);
$this->assertHttpResponseWithMessage($response, 400, 'Sorry, unrecognized username or password.', $format);
}
// Try one successful attempt for user 2, it should not trigger any
// flood control.
$this->drupalLogin($user2);
$this->drupalLogout();
// Try one more attempt for user 1, it should be rejected, even if the
// correct password has been used.
$response = $this->loginRequest($user1->getAccountName(), $user1->passRaw, $format);
// Depending on the uid_only setting the error message will be different.
if ($uid_only_setting) {
$expected_message = 'There have been more than 3 failed login attempts for this account. It is temporarily blocked. Try again later or request a new password.';
$expected_log = 'Flood control blocked login attempt for uid %uid';
}
else {
$expected_message = 'Too many failed login attempts from your IP address. This IP address is temporarily blocked.';
$expected_log = 'Flood control blocked login attempt for uid %uid from %ip';
}
$this->assertHttpResponseWithMessage($response, 403, $expected_message, $format);
$last_log = $database->select('watchdog', 'w')
->fields('w', ['message'])
->condition('type', 'user')
->orderBy('wid', 'DESC')
->range(0, 1)
->execute()
->fetchField();
$this->assertEquals($expected_log, $last_log, 'A watchdog message was logged for the login attempt blocked by flood control per user.');
}
}
/**
* Executes a logout HTTP request for a given serialization format.
*
* @param string $format
* The format to use to make the request.
* @param string $logout_token
* The csrf token for user logout.
*
* @return \Psr\Http\Message\ResponseInterface
* The HTTP response.
*/
protected function logoutRequest($format = 'json', $logout_token = '') {
/** @var \GuzzleHttp\Client $client */
$client = $this->container->get('http_client');
$user_logout_url = Url::fromRoute('user.logout.http')
->setRouteParameter('_format', $format)
->setAbsolute();
if ($logout_token) {
$user_logout_url->setOption('query', ['token' => $logout_token]);
}
$post_options = [
'headers' => [
'Accept' => "application/$format",
],
'http_errors' => FALSE,
'cookies' => $this->cookies,
];
$response = $client->post($user_logout_url->toString(), $post_options);
return $response;
}
/**
* Tests csrf protection of User Logout route for given serialization format.
*/
public function doTestLogoutCsrfProtection(string $format): void {
$client = \Drupal::httpClient();
$login_status_url = $this->getLoginStatusUrlString();
$account = $this->drupalCreateUser();
$name = $account->getAccountName();
$pass = $account->passRaw;
$response = $this->loginRequest($name, $pass, $format);
$this->assertEquals(200, $response->getStatusCode());
$result_data = $this->serializer->decode((string) $response->getBody(), $format);
$logout_token = $result_data['logout_token'];
// Test third party site posting to current site with logout request.
// This should not logout the current user because it lacks the CSRF
// token.
$response = $this->logoutRequest($format);
$this->assertEquals(403, $response->getStatusCode());
// Ensure still logged in.
$response = $client->get($login_status_url, ['cookies' => $this->cookies]);
$this->assertHttpResponse($response, 200, UserAuthenticationController::LOGGED_IN);
// Try with an incorrect token.
$response = $this->logoutRequest($format, 'not-the-correct-token');
$this->assertEquals(403, $response->getStatusCode());
// Ensure still logged in.
$response = $client->get($login_status_url, ['cookies' => $this->cookies]);
$this->assertHttpResponse($response, 200, UserAuthenticationController::LOGGED_IN);
// Try a logout request with correct token.
$response = $this->logoutRequest($format, $logout_token);
$this->assertEquals(204, $response->getStatusCode());
// Ensure actually logged out.
$response = $client->get($login_status_url, ['cookies' => $this->cookies]);
$this->assertHttpResponse($response, 200, UserAuthenticationController::LOGGED_OUT);
}
/**
* Gets the URL string for checking login for a given serialization format.
*
* @param string $format
* The format to use to make the request.
*
* @return string
* The URL string.
*/
protected function getLoginStatusUrlString($format = 'json') {
$user_login_status_url = Url::fromRoute('user.login_status.http');
$user_login_status_url->setRouteParameter('_format', $format);
$user_login_status_url->setAbsolute();
return $user_login_status_url->toString();
}
/**
* Do password reset testing for given format and account.
*
* @param string $format
* Serialization format.
* @param \Drupal\user\UserInterface $account
* Test account.
*/
protected function doTestPasswordReset($format, $account) {
$response = $this->passwordRequest([], $format);
$this->assertHttpResponseWithMessage($response, 400, 'Missing credentials.name or credentials.mail', $format);
$response = $this->passwordRequest(['name' => 'drama llama'], $format);
$this->assertEquals(200, $response->getStatusCode());
$response = $this->passwordRequest(['mail' => 'llama@drupal.org'], $format);
$this->assertEquals(200, $response->getStatusCode());
$account
->block()
->save();
$response = $this->passwordRequest(['name' => $account->getAccountName()], $format);
$this->assertEquals(200, $response->getStatusCode());
// Check that the proper warning has been logged.
$arguments = [
'%identifier' => $account->getAccountName(),
];
$logged = Database::getConnection()->select('watchdog')
->fields('watchdog', ['variables'])
->condition('type', 'user')
->condition('message', 'Unable to send password reset email for blocked or not yet activated user %identifier.')
->orderBy('wid', 'DESC')
->range(0, 1)
->execute()
->fetchField();
$this->assertEquals(serialize($arguments), $logged);
$response = $this->passwordRequest(['mail' => $account->getEmail()], $format);
$this->assertEquals(200, $response->getStatusCode());
// Check that the proper warning has been logged.
$arguments = [
'%identifier' => $account->getEmail(),
];
$logged = Database::getConnection()->select('watchdog')
->fields('watchdog', ['variables'])
->condition('type', 'user')
->condition('message', 'Unable to send password reset email for blocked or not yet activated user %identifier.')
->orderBy('wid', 'DESC')
->range(0, 1)
->execute()
->fetchField();
$this->assertEquals(serialize($arguments), $logged);
$account
->activate()
->save();
$response = $this->passwordRequest(['name' => $account->getAccountName()], $format);
$this->assertEquals(200, $response->getStatusCode());
$this->loginFromResetEmail();
$this->drupalLogout();
$response = $this->passwordRequest(['mail' => $account->getEmail()], $format);
$this->assertEquals(200, $response->getStatusCode());
$this->loginFromResetEmail();
$this->drupalLogout();
}
/**
* Login from reset password email.
*/
protected function loginFromResetEmail() {
$_emails = $this->drupalGetMails();
$email = end($_emails);
$urls = [];
preg_match('#.+user/reset/.+#', $email['body'], $urls);
$resetURL = $urls[0];
$this->drupalGet($resetURL);
$this->submitForm([], 'Log in');
$this->assertSession()->pageTextContains('You have just used your one-time login link. It is no longer necessary to use this link to log in. It is recommended that you set your password.');
}
}

View File

@@ -0,0 +1,359 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\Core\Test\AssertMailTrait;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
use Drupal\user\Entity\User;
use Drupal\user\UserInterface;
/**
* Ensure that login works as expected.
*
* @group user
*/
class UserLoginTest extends BrowserTestBase {
use AssertMailTrait {
getMails as drupalGetMails;
}
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $modules = ['dblog'];
/**
* Tests login with destination.
*/
public function testLoginCacheTagsAndDestination(): void {
$this->drupalGet('user/login');
// The user login form says "Enter your <site name> username.", hence it
// depends on config:system.site, and its cache tags should be present.
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'config:system.site');
$user = $this->drupalCreateUser([]);
$this->drupalGet('user/login', ['query' => ['destination' => 'foo']]);
$edit = ['name' => $user->getAccountName(), 'pass' => $user->passRaw];
$this->submitForm($edit, 'Log in');
$this->assertSession()->addressEquals('foo');
}
/**
* Tests the global login flood control.
*/
public function testGlobalLoginFloodControl(): void {
$this->config('user.flood')
->set('ip_limit', 10)
// Set a high per-user limit out so that it is not relevant in the test.
->set('user_limit', 4000)
->save();
$user1 = $this->drupalCreateUser([]);
$incorrect_user1 = clone $user1;
$incorrect_user1->passRaw .= 'incorrect';
// Try 2 failed logins.
for ($i = 0; $i < 2; $i++) {
$this->assertFailedLogin($incorrect_user1);
}
// A successful login will not reset the IP-based flood control count.
$this->drupalLogin($user1);
$this->drupalLogout();
// Try 8 more failed logins, they should not trigger the flood control
// mechanism.
for ($i = 0; $i < 8; $i++) {
$this->assertFailedLogin($incorrect_user1);
}
// The next login trial should result in an IP-based flood error message.
$this->assertFailedLogin($incorrect_user1, 'ip');
// A login with the correct password should also result in a flood error
// message.
$this->assertFailedLogin($user1, 'ip');
// A login attempt after resetting the password should still fail, since the
// IP-based flood control count is not cleared after a password reset.
$this->resetUserPassword($user1);
$this->drupalLogout();
$this->assertFailedLogin($user1, 'ip');
$this->assertSession()->responseContains('Too many failed login attempts from your IP address.');
}
/**
* Tests the per-user login flood control.
*/
public function testPerUserLoginFloodControl(): void {
$this->config('user.flood')
// Set a high global limit out so that it is not relevant in the test.
->set('ip_limit', 4000)
->set('user_limit', 3)
->save();
$user1 = $this->drupalCreateUser([]);
$incorrect_user1 = clone $user1;
$incorrect_user1->passRaw .= 'incorrect';
$user2 = $this->drupalCreateUser([]);
// Try 2 failed logins.
for ($i = 0; $i < 2; $i++) {
$this->assertFailedLogin($incorrect_user1);
}
// We're not going to test resetting the password which should clear the
// flood table and allow the user to log in again.
$this->drupalLogin($user1);
$this->drupalLogout();
// Try 3 failed logins for user 1, they will not trigger flood control.
for ($i = 0; $i < 3; $i++) {
$this->assertFailedLogin($incorrect_user1);
}
// Try one successful attempt for user 2, it should not trigger any
// flood control.
$this->drupalLogin($user2);
$this->drupalLogout();
// Try one more attempt for user 1, it should be rejected, even if the
// correct password has been used.
$this->assertFailedLogin($user1, 'user');
$this->resetUserPassword($user1);
$this->drupalLogout();
// Try to log in as user 1, it should be successful.
$this->drupalLogin($user1);
$this->assertSession()->responseContains('Member for');
}
/**
* Tests user password is re-hashed upon login after changing $count_log2.
*/
public function testPasswordRehashOnLogin(): void {
// Retrieve instance of password hashing algorithm.
$password_hasher = $this->container->get('password');
// Create a new user and authenticate.
$account = $this->drupalCreateUser([]);
$password = $account->passRaw;
$this->drupalLogin($account);
$this->drupalLogout();
// Load the stored user. The password hash shouldn't need a rehash.
$user_storage = $this->container->get('entity_type.manager')->getStorage('user');
$account = User::load($account->id());
// Check that the stored password doesn't need rehash.
$this->assertFalse($password_hasher->needsRehash($account->getPassword()));
// The current hashing cost is set to 10 in the container. Increase cost by
// one, by enabling a module containing the necessary container changes.
\Drupal::service('module_installer')->install(['user_custom_pass_hash_params_test']);
$this->resetAll();
// Reload the hashing service after container changes.
$password_hasher = $this->container->get('password');
// Check that the stored password does need rehash.
$this->assertTrue($password_hasher->needsRehash($account->getPassword()));
$account->passRaw = $password;
$this->drupalLogin($account);
// Load the stored user, which should have a different password hash now.
$user_storage->resetCache([$account->id()]);
$account = $user_storage->load($account->id());
// Check that the stored password doesn't need rehash.
$this->assertFalse($password_hasher->needsRehash($account->getPassword()));
$this->assertTrue($password_hasher->check($password, $account->getPassword()));
}
/**
* Tests log in with a maximum length and a too long password.
*/
public function testPasswordLengthLogin(): void {
// Create a new user and authenticate.
$account = $this->drupalCreateUser([]);
$current_password = $account->passRaw;
$this->drupalLogin($account);
// Use the length specified in
// \Drupal\Core\Render\Element\Password::getInfo().
$length = 128;
$current_password = $this->doPasswordLengthLogin($account, $current_password, $length);
$this->assertSession()->pageTextNotContains('Password cannot be longer than');
$this->assertSession()->pageTextContains('Member for');
$this->doPasswordLengthLogin($account, $current_password, $length + 1);
$this->assertSession()->pageTextContains('Password cannot be longer than ' . $length . ' characters but is currently ' . ($length + 1) . ' characters long.');
$this->assertSession()->pageTextNotContains('Member for');
}
/**
* Helper to test log in with a maximum length password.
*
* @param \Drupal\user\UserInterface $account
* An object containing the user account.
* @param string $current_password
* The current password associated with the user.
* @param int $length
* The length of the password.
*
* @return string
* The new password associated with the user.
*/
public function doPasswordLengthLogin(UserInterface $account, string $current_password, int $length) {
$new_password = \Drupal::service('password_generator')->generate($length);
$uid = $account->id();
$edit = [
'current_pass' => $current_password,
'mail' => $account->getEmail(),
'pass[pass1]' => $new_password,
'pass[pass2]' => $new_password,
];
// Change the password.
$this->drupalGet("user/$uid/edit");
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('The changes have been saved.');
$this->drupalLogout();
// Login with new password.
$this->drupalGet('user/login');
$edit = [
'name' => $account->getAccountName(),
'pass' => $new_password,
];
$this->submitForm($edit, 'Log in');
return $new_password;
}
/**
* Tests with a browser that denies cookies.
*/
public function testCookiesNotAccepted(): void {
$this->drupalGet('user/login');
$form_build_id = $this->getSession()->getPage()->findField('form_build_id');
$account = $this->drupalCreateUser([]);
$post = [
'form_id' => 'user_login_form',
'form_build_id' => $form_build_id,
'name' => $account->getAccountName(),
'pass' => $account->passRaw,
'op' => 'Log in',
];
$url = $this->buildUrl(Url::fromRoute('user.login'));
/** @var \Psr\Http\Message\ResponseInterface $response */
$response = $this->getHttpClient()->post($url, [
'form_params' => $post,
'http_errors' => FALSE,
'cookies' => FALSE,
'allow_redirects' => FALSE,
]);
// Follow the location header.
$this->drupalGet($response->getHeader('location')[0]);
$this->assertSession()->statusCodeEquals(403);
$this->assertSession()->pageTextContains('To log in to this site, your browser must accept cookies from the domain');
}
/**
* Make an unsuccessful login attempt.
*
* @param \Drupal\user\Entity\User $account
* A user object with name and passRaw attributes for the login attempt.
* @param string $flood_trigger
* (optional) Whether or not to expect that the flood control mechanism
* will be triggered. Defaults to NULL.
* - Set to 'user' to expect a 'too many failed logins error.
* - Set to any value to expect an error for too many failed logins per IP.
* - Set to NULL to expect a failed login.
*
* @internal
*/
public function assertFailedLogin(User $account, ?string $flood_trigger = NULL): void {
$database = \Drupal::database();
$edit = [
'name' => $account->getAccountName(),
'pass' => $account->passRaw,
];
$this->drupalGet('user/login');
$this->submitForm($edit, 'Log in');
if (isset($flood_trigger)) {
$this->assertSession()->statusCodeEquals(403);
$this->assertSession()->fieldNotExists('pass');
$last_log = $database->select('watchdog', 'w')
->fields('w', ['message'])
->condition('type', 'user')
->orderBy('wid', 'DESC')
->range(0, 1)
->execute()
->fetchField();
if ($flood_trigger == 'user') {
$this->assertSession()->pageTextMatches("/There (has|have) been more than \w+ failed login attempt.* for this account. It is temporarily blocked. Try again later or request a new password./");
$this->assertSession()->elementExists('css', 'body.maintenance-page');
$this->assertSession()->linkExists("request a new password");
$this->assertSession()->linkByHrefExists(Url::fromRoute('user.pass')->toString());
$this->assertEquals('Flood control blocked login attempt for uid %uid from %ip', $last_log, 'A watchdog message was logged for the login attempt blocked by flood control per user.');
}
else {
// No uid, so the limit is IP-based.
$this->assertSession()->pageTextContains("Too many failed login attempts from your IP address. This IP address is temporarily blocked. Try again later or request a new password.");
$this->assertSession()->elementExists('css', 'body.maintenance-page');
$this->assertSession()->linkExists("request a new password");
$this->assertSession()->linkByHrefExists(Url::fromRoute('user.pass')->toString());
$this->assertEquals('Flood control blocked login attempt from %ip', $last_log, 'A watchdog message was logged for the login attempt blocked by flood control per IP.');
}
}
else {
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->fieldValueEquals('pass', '');
$this->assertSession()->pageTextContains('Unrecognized username or password. Forgot your password?');
}
}
/**
* Reset user password.
*
* @param object $user
* A user object.
*/
public function resetUserPassword($user) {
$this->drupalGet('user/password');
$edit['name'] = $user->getDisplayName();
$this->submitForm($edit, 'Submit');
$_emails = $this->drupalGetMails();
$email = end($_emails);
$urls = [];
preg_match('#.+user/reset/.+#', $email['body'], $urls);
$resetURL = $urls[0];
$this->drupalGet($resetURL);
$this->submitForm([], 'Log in');
}
/**
* Tests that user login form has the autocomplete attributes.
*/
public function testAutocompleteHtmlAttributes(): void {
$this->drupalGet('user/login');
$name_field = $this->getSession()->getPage()->findField('name');
$pass_field = $this->getSession()->getPage()->findField('pass');
$this->assertEquals('username', $name_field->getAttribute('autocomplete'));
$this->assertEquals('current-password', $pass_field->getAttribute('autocomplete'));
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Tests user logout.
*
* @group user
*/
class UserLogoutTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['user', 'block'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp() : void {
parent::setUp();
$this->placeBlock('system_menu_block:account');
}
/**
* Tests user logout functionality.
*/
public function testLogout(): void {
$account = $this->createUser();
$this->drupalLogin($account);
// Test missing csrf token does not log the user out.
$logoutUrl = Url::fromRoute('user.logout');
$confirmUrl = Url::fromRoute('user.logout.confirm');
$this->drupalGet($logoutUrl);
$this->assertTrue($this->drupalUserIsLoggedIn($account));
$this->assertSession()->addressEquals($confirmUrl);
// Test invalid csrf token does not log the user out.
$this->drupalGet($logoutUrl, ['query' => ['token' => '123']]);
$this->assertTrue($this->drupalUserIsLoggedIn($account));
$this->assertSession()->addressEquals($confirmUrl);
// Submitting the confirmation form correctly logs the user out.
$this->submitForm([], 'Log out');
$this->assertFalse($this->drupalUserIsLoggedIn($account));
$this->drupalResetSession();
$this->drupalLogin($account);
// Test with valid logout link.
$this->drupalGet('user');
$this->getSession()->getPage()->clickLink('Log out');
$this->assertFalse($this->drupalUserIsLoggedIn($account));
// Test hitting the confirm form while logged out redirects to the
// frontpage.
$this->drupalGet($confirmUrl);
$this->assertSession()->addressEquals(Url::fromRoute('<front>'));
}
}

View File

@@ -0,0 +1,656 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\Core\Database\Database;
use Drupal\Core\Test\AssertMailTrait;
use Drupal\Core\Url;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\BrowserTestBase;
use Drupal\user\Entity\User;
use Drupal\user\UserInterface;
/**
* Ensure that password reset methods work as expected.
*
* @group user
*/
class UserPasswordResetTest extends BrowserTestBase {
use AssertMailTrait {
getMails as drupalGetMails;
}
/**
* The user object to test password resetting.
*
* @var \Drupal\user\UserInterface
*/
protected $account;
/**
* Language manager object.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['block', 'language'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Enable page caching.
$config = $this->config('system.performance');
$config->set('cache.page.max_age', 3600);
$config->save();
$this->drupalPlaceBlock('system_menu_block:account');
// Create a user.
$account = $this->drupalCreateUser();
// Activate user by logging in.
$this->drupalLogin($account);
$this->account = User::load($account->id());
$this->account->passRaw = $account->passRaw;
$this->drupalLogout();
// Set the last login time that is used to generate the one-time link so
// that it is definitely over a second ago.
$account->login = \Drupal::time()->getRequestTime() - mt_rand(10, 100000);
Database::getConnection()->update('users_field_data')
->fields(['login' => $account->getLastLoginTime()])
->condition('uid', $account->id())
->execute();
}
/**
* Tests password reset functionality.
*/
public function testUserPasswordReset(): void {
// Verify that accessing the password reset form without having the session
// variables set results in an access denied message.
$this->drupalGet(Url::fromRoute('user.reset.form', ['uid' => $this->account->id()]));
$this->assertSession()->statusCodeEquals(403);
// Try to reset the password for a completely invalid username.
$this->drupalGet('user/password');
$long_name = $this->randomMachineName(UserInterface::USERNAME_MAX_LENGTH + 10);
$edit = ['name' => $long_name];
$this->submitForm($edit, 'Submit');
$this->assertCount(0, $this->drupalGetMails(['id' => 'user_password_reset']), 'No email was sent when requesting a password for an invalid user name.');
$this->assertSession()->pageTextContains("The username or email address is invalid.");
// Try to reset the password for an invalid account.
$this->drupalGet('user/password');
$random_name = $this->randomMachineName();
$edit = ['name' => $random_name];
$this->submitForm($edit, 'Submit');
$this->assertNoValidPasswordReset($random_name);
// Try to reset the password for a valid email address longer than
// UserInterface::USERNAME_MAX_LENGTH (invalid username, valid email).
// This should pass validation and print the generic message.
$this->drupalGet('user/password');
$long_name = $this->randomMachineName(UserInterface::USERNAME_MAX_LENGTH) . '@example.com';
$edit = ['name' => $long_name];
$this->submitForm($edit, 'Submit');
$this->assertNoValidPasswordReset($long_name);
// Reset the password by username via the password reset page.
$this->drupalGet('user/password');
$edit = ['name' => $this->account->getAccountName()];
$this->submitForm($edit, 'Submit');
$this->assertValidPasswordReset($edit['name']);
$resetURL = $this->getResetURL();
$this->drupalGet($resetURL);
// Ensure that the current URL does not contain the hash and timestamp.
$this->assertSession()->addressEquals(Url::fromRoute('user.reset.form', ['uid' => $this->account->id()]));
$this->assertSession()->responseHeaderDoesNotExist('X-Drupal-Cache');
// Ensure the password reset URL is not cached.
$this->drupalGet($resetURL);
$this->assertSession()->responseHeaderDoesNotExist('X-Drupal-Cache');
// Check the one-time login page.
$this->assertSession()->pageTextContains($this->account->getAccountName());
$this->assertSession()->pageTextContains('This login can be used only once.');
$this->assertSession()->titleEquals('Reset password | Drupal');
// Check successful login.
$this->submitForm([], 'Log in');
$this->assertSession()->linkExists('Log out');
$this->assertSession()->titleEquals($this->account->getAccountName() . ' | Drupal');
// Change the forgotten password.
$password = \Drupal::service('password_generator')->generate();
$edit = ['pass[pass1]' => $password, 'pass[pass2]' => $password];
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('The changes have been saved.');
// Verify that the password reset session has been destroyed.
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("Your current password is missing or incorrect; it's required to change the Password.");
// Log out, and try to log in again using the same one-time link.
$this->drupalLogout();
$this->drupalGet($resetURL);
$this->assertSession()->pageTextContains('You have tried to use a one-time login link that has either been used or is no longer valid. Request a new one using the form below.');
$this->drupalGet($resetURL . '/login');
$this->assertSession()->pageTextContains('You have tried to use a one-time login link that has either been used or is no longer valid. Request a new one using the form below.');
// Request a new password again, this time using the email address.
// Count email messages before to compare with after.
$before = count($this->drupalGetMails(['id' => 'user_password_reset']));
$this->drupalGet('user/password');
$edit = ['name' => $this->account->getEmail()];
$this->submitForm($edit, 'Submit');
$this->assertValidPasswordReset($edit['name']);
$this->assertCount($before + 1, $this->drupalGetMails(['id' => 'user_password_reset']), 'Email sent when requesting password reset using email address.');
// Visit the user edit page without pass-reset-token and make sure it does
// not cause an error.
$resetURL = $this->getResetURL();
$this->drupalGet($resetURL);
$this->submitForm([], 'Log in');
$this->drupalGet('user/' . $this->account->id() . '/edit');
$this->assertSession()->pageTextNotContains('Expected user_string to be a string, NULL given');
$this->drupalLogout();
// Create a password reset link as if the request time was 60 seconds older than the allowed limit.
$timeout = $this->config('user.settings')->get('password_reset_timeout');
$bogus_timestamp = \Drupal::time()->getRequestTime() - $timeout - 60;
$_uid = $this->account->id();
$this->drupalGet("user/reset/$_uid/$bogus_timestamp/" . user_pass_rehash($this->account, $bogus_timestamp));
$this->assertSession()->pageTextContains('You have tried to use a one-time login link that has expired. Request a new one using the form below.');
$this->drupalGet("user/reset/$_uid/$bogus_timestamp/" . user_pass_rehash($this->account, $bogus_timestamp) . '/login');
$this->assertSession()->pageTextContains('You have tried to use a one-time login link that has expired. Request a new one using the form below.');
// Create a user, block the account, and verify that a login link is denied.
$timestamp = \Drupal::time()->getRequestTime() - 1;
$blocked_account = $this->drupalCreateUser()->block();
$blocked_account->save();
$this->drupalGet("user/reset/" . $blocked_account->id() . "/$timestamp/" . user_pass_rehash($blocked_account, $timestamp));
$this->assertSession()->statusCodeEquals(403);
$this->drupalGet("user/reset/" . $blocked_account->id() . "/$timestamp/" . user_pass_rehash($blocked_account, $timestamp) . '/login');
$this->assertSession()->statusCodeEquals(403);
// Verify a blocked user can not request a new password.
$this->drupalGet('user/password');
// Count email messages before to compare with after.
$before = count($this->drupalGetMails(['id' => 'user_password_reset']));
$edit = ['name' => $blocked_account->getAccountName()];
$this->submitForm($edit, 'Submit');
$this->assertCount($before, $this->drupalGetMails(['id' => 'user_password_reset']), 'No email was sent when requesting password reset for a blocked account');
// Verify a password reset link is invalidated when the user's email address changes.
$this->drupalGet('user/password');
$edit = ['name' => $this->account->getAccountName()];
$this->submitForm($edit, 'Submit');
$old_email_reset_link = $this->getResetURL();
$this->account->setEmail("1" . $this->account->getEmail());
$this->account->save();
$this->drupalGet($old_email_reset_link);
$this->assertSession()->pageTextContains('You have tried to use a one-time login link that has either been used or is no longer valid. Request a new one using the form below.');
$this->drupalGet($old_email_reset_link . '/login');
$this->assertSession()->pageTextContains('You have tried to use a one-time login link that has either been used or is no longer valid. Request a new one using the form below.');
// Verify a password reset link will automatically log a user when /login is
// appended.
$this->drupalGet('user/password');
$edit = ['name' => $this->account->getAccountName()];
$this->submitForm($edit, 'Submit');
$reset_url = $this->getResetURL();
$this->drupalGet($reset_url . '/login');
$this->assertSession()->linkExists('Log out');
$this->assertSession()->titleEquals($this->account->getAccountName() . ' | Drupal');
// Ensure blocked and deleted accounts can't access the user.reset.login
// route.
$this->drupalLogout();
$timestamp = \Drupal::time()->getRequestTime() - 1;
$blocked_account = $this->drupalCreateUser()->block();
$blocked_account->save();
$this->drupalGet("user/reset/" . $blocked_account->id() . "/$timestamp/" . user_pass_rehash($blocked_account, $timestamp) . '/login');
$this->assertSession()->statusCodeEquals(403);
$blocked_account->delete();
$this->drupalGet("user/reset/" . $blocked_account->id() . "/$timestamp/" . user_pass_rehash($blocked_account, $timestamp) . '/login');
$this->assertSession()->statusCodeEquals(403);
}
/**
* Tests password reset functionality when user has set preferred language.
*
* @dataProvider languagePrefixTestProvider
*/
public function testUserPasswordResetPreferredLanguage($setPreferredLangcode, $activeLangcode, $prefix, $visitingUrl, $expectedResetUrl, $unexpectedResetUrl): void {
// Set two new languages.
ConfigurableLanguage::createFromLangcode('fr')->save();
ConfigurableLanguage::createFromLangcode('zh-hant')->save();
$this->languageManager = \Drupal::languageManager();
// Set language prefixes.
$config = $this->config('language.negotiation');
$config->set('url.prefixes', ['en' => '', 'fr' => 'fr', 'zh-hant' => 'zh'])->save();
$this->rebuildContainer();
$this->account->preferred_langcode = $setPreferredLangcode;
$this->account->save();
$this->assertSame($setPreferredLangcode, $this->account->getPreferredLangcode(FALSE));
// Test Default langcode is different from active langcode when visiting different.
if ($setPreferredLangcode !== 'en') {
$this->drupalGet($prefix . '/user/password');
$this->assertSame($activeLangcode, $this->getSession()->getResponseHeader('Content-language'));
$this->assertSame('en', $this->languageManager->getDefaultLanguage()->getId());
}
// Test password reset with language prefixes.
$this->drupalGet($visitingUrl);
$edit = ['name' => $this->account->getAccountName()];
$this->submitForm($edit, 'Submit');
$this->assertValidPasswordReset($edit['name']);
$resetURL = $this->getResetURL();
$this->assertStringContainsString($expectedResetUrl, $resetURL);
$this->assertStringNotContainsString($unexpectedResetUrl, $resetURL);
}
/**
* Data provider for testUserPasswordResetPreferredLanguage().
*
* @return array
*/
public static function languagePrefixTestProvider() {
return [
'Test language prefix set as \'\', visiting default with preferred language as en' => [
'setPreferredLangcode' => 'en',
'activeLangcode' => 'en',
'prefix' => '',
'visitingUrl' => 'user/password',
'expectedResetUrl' => 'user/reset',
'unexpectedResetUrl' => 'en/user/reset',
],
'Test language prefix set as fr, visiting zh with preferred language as fr' => [
'setPreferredLangcode' => 'fr',
'activeLangcode' => 'fr',
'prefix' => 'fr',
'visitingUrl' => 'zh/user/password',
'expectedResetUrl' => 'fr/user/reset',
'unexpectedResetUrl' => 'zh/user/reset',
],
'Test language prefix set as zh, visiting zh with preferred language as \'\'' => [
'setPreferredLangcode' => '',
'activeLangcode' => 'zh-hant',
'prefix' => 'zh',
'visitingUrl' => 'zh/user/password',
'expectedResetUrl' => 'user/reset',
'unexpectedResetUrl' => 'zh/user/reset',
],
];
}
/**
* Retrieves password reset email and extracts the login link.
*/
public function getResetURL() {
// Assume the most recent email.
$_emails = $this->drupalGetMails();
$email = end($_emails);
$urls = [];
preg_match('#.+user/reset/.+#', $email['body'], $urls);
return $urls[0];
}
/**
* Tests user password reset while logged in.
*/
public function testUserPasswordResetLoggedIn(): void {
$another_account = $this->drupalCreateUser();
$this->drupalLogin($another_account);
$this->drupalGet('user/password');
$this->submitForm([], 'Submit');
// Click the reset URL while logged and change our password.
$resetURL = $this->getResetURL();
// Log in as a different user.
$this->drupalLogin($this->account);
$this->drupalGet($resetURL);
$this->assertSession()->pageTextContains("Another user ({$this->account->getAccountName()}) is already logged into the site on this computer, but you tried to use a one-time link for user {$another_account->getAccountName()}. Log out and try using the link again.");
$this->assertSession()->linkExists('Log out');
$this->assertSession()->linkByHrefExists(Url::fromRoute('user.logout')->toString());
// Verify that the invalid password reset page does not show the user name.
$attack_reset_url = "user/reset/" . $another_account->id() . "/1/1";
$this->drupalGet($attack_reset_url);
$this->assertSession()->pageTextNotContains($another_account->getAccountName());
$this->assertSession()->addressEquals('user/' . $this->account->id());
$this->assertSession()->pageTextContains('The one-time login link you clicked is invalid.');
$another_account->delete();
$this->drupalGet($resetURL);
$this->assertSession()->pageTextContains('The one-time login link you clicked is invalid.');
// Log in.
$this->drupalLogin($this->account);
// Reset the password by username via the password reset page.
$this->drupalGet('user/password');
$this->submitForm([], 'Submit');
// Click the reset URL while logged and change our password.
$resetURL = $this->getResetURL();
$this->drupalGet($resetURL);
$this->submitForm([], 'Log in');
// Change the password.
$password = \Drupal::service('password_generator')->generate();
$edit = ['pass[pass1]' => $password, 'pass[pass2]' => $password];
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('The changes have been saved.');
// Logged in users should not be able to access the user.reset.login or the
// user.reset.form routes.
$timestamp = \Drupal::time()->getRequestTime() - 1;
$this->drupalGet("user/reset/" . $this->account->id() . "/$timestamp/" . user_pass_rehash($this->account, $timestamp) . '/login');
$this->assertSession()->statusCodeEquals(403);
$this->drupalGet("user/reset/" . $this->account->id());
$this->assertSession()->statusCodeEquals(403);
}
/**
* Tests the text box on incorrect login via link to password reset page.
*/
public function testUserResetPasswordTextboxNotFilled(): void {
$this->drupalGet('user/login');
$edit = [
'name' => $this->randomMachineName(),
'pass' => $this->randomMachineName(),
];
$this->drupalGet('user/login');
$this->submitForm($edit, 'Log in');
$this->assertSession()->pageTextContains("Unrecognized username or password. Forgot your password?");
$this->assertSession()->linkExists("Forgot your password?");
// Verify we don't pass the username as a query parameter.
$this->assertSession()->linkByHrefNotExists(Url::fromRoute('user.pass', [], ['query' => ['name' => $edit['name']]])->toString());
$this->assertSession()->linkByHrefExists(Url::fromRoute('user.pass')->toString());
unset($edit['pass']);
// Verify the field is empty by default.
$this->drupalGet('user/password');
$this->assertSession()->fieldValueEquals('name', '');
// Ensure the name field value is not cached.
$this->drupalGet('user/password', ['query' => ['name' => $edit['name']]]);
$this->assertSession()->fieldValueEquals('name', $edit['name']);
$this->drupalGet('user/password');
$this->assertSession()->fieldValueNotEquals('name', $edit['name']);
}
/**
* Tests password reset flood control for one user.
*/
public function testUserResetPasswordUserFloodControl(): void {
\Drupal::configFactory()->getEditable('user.flood')
->set('user_limit', 3)
->save();
$edit = ['name' => $this->account->getAccountName()];
// Count email messages before to compare with after.
$before = count($this->drupalGetMails(['id' => 'user_password_reset']));
// Try 3 requests that should not trigger flood control.
for ($i = 0; $i < 3; $i++) {
$this->drupalGet('user/password');
$this->submitForm($edit, 'Submit');
$this->assertValidPasswordReset($edit['name']);
}
// Ensure 3 emails were sent.
$this->assertCount($before + 3, $this->drupalGetMails(['id' => 'user_password_reset']), '3 emails sent without triggering flood control.');
// The next request should trigger flood control.
$this->drupalGet('user/password');
$this->submitForm($edit, 'Submit');
// Ensure no further emails were sent.
$this->assertCount($before + 3, $this->drupalGetMails(['id' => 'user_password_reset']), 'No further email was sent after triggering flood control.');
}
/**
* Tests password reset flood control for one IP.
*/
public function testUserResetPasswordIpFloodControl(): void {
\Drupal::configFactory()->getEditable('user.flood')
->set('ip_limit', 3)
->save();
// Try 3 requests that should not trigger flood control.
for ($i = 0; $i < 3; $i++) {
$this->drupalGet('user/password');
$random_name = $this->randomMachineName();
$edit = ['name' => $random_name];
$this->submitForm($edit, 'Submit');
// Because we're testing with a random name, the password reset will not be valid.
$this->assertNoValidPasswordReset($random_name);
$this->assertNoPasswordIpFlood();
}
// The next request should trigger flood control.
$this->drupalGet('user/password');
$edit = ['name' => $this->randomMachineName()];
$this->submitForm($edit, 'Submit');
$this->assertPasswordIpFlood();
}
/**
* Tests user password reset flood control is cleared on successful reset.
*/
public function testUserResetPasswordUserFloodControlIsCleared(): void {
\Drupal::configFactory()->getEditable('user.flood')
->set('user_limit', 3)
->save();
$edit = ['name' => $this->account->getAccountName()];
// Count email messages before to compare with after.
$before = count($this->drupalGetMails(['id' => 'user_password_reset']));
// Try 3 requests that should not trigger flood control.
for ($i = 0; $i < 3; $i++) {
$this->drupalGet('user/password');
$this->submitForm($edit, 'Submit');
$this->assertValidPasswordReset($edit['name']);
}
// Ensure 3 emails were sent.
$this->assertCount($before + 3, $this->drupalGetMails(['id' => 'user_password_reset']), '3 emails sent without triggering flood control.');
// Use the last password reset URL which was generated.
$reset_url = $this->getResetURL();
$this->drupalGet($reset_url . '/login');
$this->assertSession()->linkExists('Log out');
$this->assertSession()->titleEquals($this->account->getAccountName() . ' | Drupal');
$this->drupalLogout();
// The next request should *not* trigger flood control, since a successful
// password reset should have cleared flood events for this user.
$this->drupalGet('user/password');
$this->submitForm($edit, 'Submit');
$this->assertValidPasswordReset($edit['name']);
// Ensure another email was sent.
$this->assertCount($before + 4, $this->drupalGetMails(['id' => 'user_password_reset']), 'Another email was sent after clearing flood control.');
}
/**
* Tests user password reset flood control is cleared on admin reset.
*/
public function testUserResetPasswordUserFloodControlAdmin(): void {
$admin_user = $this->drupalCreateUser([
'administer account settings',
'administer users',
]);
\Drupal::configFactory()->getEditable('user.flood')
->set('user_limit', 3)
->save();
$edit = [
'name' => $this->account->getAccountName(),
'pass' => 'wrong_password',
];
// Try 3 requests that should not trigger flood control.
for ($i = 0; $i < 3; $i++) {
$this->drupalGet('user/login');
$this->submitForm($edit, 'Log in');
$this->assertSession()->pageTextNotContains('There have been more than 3 failed login attempts for this account. It is temporarily blocked.');
}
$this->drupalGet('user/login');
$this->submitForm($edit, 'Log in');
$this->assertSession()->pageTextContains('There have been more than 3 failed login attempts for this account. It is temporarily blocked.');
$password = $this->randomMachineName();
$edit = [
'pass[pass1]' => $password,
'pass[pass2]' => $password,
];
// Log in as admin and change the user password.
$this->drupalLogin($admin_user);
$this->drupalGet('user/' . $this->account->id() . '/edit');
$this->submitForm($edit, 'Save');
$this->drupalLogout();
$edit = [
'name' => $this->account->getAccountName(),
'pass' => $password,
];
// The next request should *not* trigger flood control, since the
// password change should have cleared flood events for this user.
$this->account->passRaw = $password;
$this->drupalLogin($this->account);
$this->assertSession()->pageTextNotContains('There have been more than 3 failed login attempts for this account. It is temporarily blocked.');
}
/**
* Helper function to make assertions about a valid password reset.
*
* @internal
*/
public function assertValidPasswordReset(string $name): void {
$this->assertSession()->pageTextContains("If $name is a valid account, an email will be sent with instructions to reset your password.");
$this->assertMail('to', $this->account->getEmail(), 'Password email sent to user.');
$subject = 'Replacement login information for ' . $this->account->getAccountName() . ' at Drupal';
$this->assertMail('subject', $subject, 'Password reset email subject is correct.');
}
/**
* Helper function to make assertions about an invalid password reset.
*
* @param string $name
* The user name.
*
* @internal
*/
public function assertNoValidPasswordReset(string $name): void {
// This message is the same as the valid reset for privacy reasons.
$this->assertSession()->pageTextContains("If $name is a valid account, an email will be sent with instructions to reset your password.");
// The difference is that no email is sent.
$this->assertCount(0, $this->drupalGetMails(['id' => 'user_password_reset']), 'No email was sent when requesting a password for an invalid account.');
}
/**
* Makes assertions about a password reset triggering IP flood control.
*
* @internal
*/
public function assertPasswordIpFlood(): void {
$this->assertSession()->pageTextContains('Too many password recovery requests from your IP address. It is temporarily blocked. Try again later or contact the site administrator.');
}
/**
* Makes assertions about a password reset not triggering IP flood control.
*
* @internal
*/
public function assertNoPasswordIpFlood(): void {
$this->assertSession()->pageTextNotContains('Too many password recovery requests from your IP address. It is temporarily blocked. Try again later or contact the site administrator.');
}
/**
* Make sure that users cannot forge password reset URLs of other users.
*/
public function testResetImpersonation(): void {
// Create two identical user accounts except for the user name. They must
// have the same empty password, so we can't use $this->drupalCreateUser().
$edit = [];
$edit['name'] = $this->randomMachineName();
$edit['mail'] = $edit['name'] . '@example.com';
$edit['status'] = 1;
$user1 = User::create($edit);
$user1->save();
$edit['name'] = $this->randomMachineName();
$user2 = User::create($edit);
$user2->save();
// Unique password hashes are automatically generated, the only way to
// change that is to update it directly in the database.
Database::getConnection()->update('users_field_data')
->fields(['pass' => NULL])
->condition('uid', [$user1->id(), $user2->id()], 'IN')
->execute();
\Drupal::entityTypeManager()->getStorage('user')->resetCache();
$user1 = User::load($user1->id());
$user2 = User::load($user2->id());
$this->assertEquals($user2->getPassword(), $user1->getPassword(), 'Both users have the same password hash.');
// The password reset URL must not be valid for the second user when only
// the user ID is changed in the URL.
$reset_url = user_pass_reset_url($user1);
$attack_reset_url = str_replace("user/reset/{$user1->id()}", "user/reset/{$user2->id()}", $reset_url);
$this->drupalGet($attack_reset_url);
// Verify that the invalid password reset page does not show the user name.
$this->assertSession()->pageTextNotContains($user2->getAccountName());
$this->assertSession()->addressEquals('user/password');
$this->assertSession()->pageTextContains('You have tried to use a one-time login link that has either been used or is no longer valid. Request a new one using the form below.');
$this->drupalGet($attack_reset_url . '/login');
// Verify that the invalid password reset page does not show the user name.
$this->assertSession()->pageTextNotContains($user2->getAccountName());
$this->assertSession()->addressEquals('user/password');
$this->assertSession()->pageTextContains('You have tried to use a one-time login link that has either been used or is no longer valid. Request a new one using the form below.');
}
/**
* Test the autocomplete attribute is present.
*/
public function testResetFormHasAutocompleteAttribute(): void {
$this->drupalGet('user/password');
$field = $this->getSession()->getPage()->findField('name');
$this->assertEquals('username', $field->getAttribute('autocomplete'));
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\user\Entity\Role;
/**
* Tests adding and removing permissions via the UI.
*
* @group user
*/
class UserPermissionsAdminTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests granting and revoking permissions via the UI sorts permissions.
*/
public function testPermissionsSorting(): void {
$role = Role::create(['id' => 'test_role', 'label' => 'Test role']);
// Start the role with a permission that is near the end of the alphabet.
$role->grantPermission('view user email addresses');
$role->save();
$this->drupalLogin($this->drupalCreateUser([
'administer permissions',
]));
$this->drupalGet('admin/people/permissions');
$this->assertSession()->statusCodeEquals(200);
// Add a permission that is near the start of the alphabet.
$this->submitForm([
'test_role[change own username]' => 1,
], 'Save permissions');
// Check that permissions are sorted alphabetically.
$storage = \Drupal::entityTypeManager()->getStorage('user_role');
/** @var \Drupal\user\Entity\Role $role */
$role = $storage->loadUnchanged($role->id());
$this->assertEquals([
'change own username',
'view user email addresses',
], $role->getPermissions());
// Remove the first permission, resulting in a single permission in the first
// key of the array.
$this->submitForm([
'test_role[change own username]' => 0,
], 'Save permissions');
/** @var \Drupal\user\Entity\Role $role */
$role = $storage->loadUnchanged($role->id());
$this->assertEquals([
'view user email addresses',
], $role->getPermissions());
}
}

View File

@@ -0,0 +1,300 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\user\RoleInterface;
use Drupal\user\Entity\Role;
/**
* Verifies role permissions can be added and removed via the permissions page.
*
* @group user
*/
class UserPermissionsTest extends BrowserTestBase {
/**
* User with admin privileges.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* User's role ID.
*
* @var string
*/
protected $rid;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->adminUser = $this->drupalCreateUser([
'administer permissions',
'access user profiles',
'administer site configuration',
'administer modules',
'administer account settings',
]);
// Find the new role ID.
$all_rids = $this->adminUser->getRoles();
unset($all_rids[array_search(RoleInterface::AUTHENTICATED_ID, $all_rids)]);
$this->rid = reset($all_rids);
}
/**
* Tests changing user permissions through the permissions pages.
*/
public function testUserPermissionChanges(): void {
$permissions_hash_generator = $this->container->get('user_permissions_hash_generator');
$storage = $this->container->get('entity_type.manager')->getStorage('user_role');
// Create an additional role and mark it as admin role.
Role::create(['is_admin' => TRUE, 'id' => 'administrator', 'label' => 'Administrator'])->save();
$storage->resetCache();
$this->drupalLogin($this->adminUser);
$rid = $this->rid;
$account = $this->adminUser;
$previous_permissions_hash = $permissions_hash_generator->generate($account);
$this->assertSame($previous_permissions_hash, $permissions_hash_generator->generate($this->loggedInUser));
// Add a permission.
$this->assertFalse($account->hasPermission('administer users'), 'User does not have "administer users" permission.');
$edit = [];
$edit[$rid . '[administer users]'] = TRUE;
$this->drupalGet('admin/people/permissions');
$this->submitForm($edit, 'Save permissions');
$this->assertSession()->pageTextContains('The changes have been saved.');
$storage->resetCache();
$this->assertTrue($account->hasPermission('administer users'), 'User now has "administer users" permission.');
$current_permissions_hash = $permissions_hash_generator->generate($account);
$this->assertSame($current_permissions_hash, $permissions_hash_generator->generate($this->loggedInUser));
$this->assertNotEquals($previous_permissions_hash, $current_permissions_hash, 'Permissions hash has changed.');
$previous_permissions_hash = $current_permissions_hash;
// Remove a permission.
$this->assertTrue($account->hasPermission('access user profiles'), 'User has "access user profiles" permission.');
$edit = [];
$edit[$rid . '[access user profiles]'] = FALSE;
$this->drupalGet('admin/people/permissions');
$this->submitForm($edit, 'Save permissions');
$this->assertSession()->pageTextContains('The changes have been saved.');
$storage->resetCache();
$this->assertFalse($account->hasPermission('access user profiles'), 'User no longer has "access user profiles" permission.');
$current_permissions_hash = $permissions_hash_generator->generate($account);
$this->assertSame($current_permissions_hash, $permissions_hash_generator->generate($this->loggedInUser));
$this->assertNotEquals($previous_permissions_hash, $current_permissions_hash, 'Permissions hash has changed.');
// Permissions can be changed using the module-specific pages with the same
// result.
$edit = [];
$edit[$rid . '[access user profiles]'] = TRUE;
$this->drupalGet('admin/people/permissions/module/user');
$this->submitForm($edit, 'Save permissions');
$this->assertSession()->pageTextContains('The changes have been saved.');
$storage->resetCache();
$this->assertTrue($account->hasPermission('access user profiles'), 'User again has "access user profiles" permission.');
$current_permissions_hash = $permissions_hash_generator->generate($account);
$this->assertSame($current_permissions_hash, $permissions_hash_generator->generate($this->loggedInUser));
$this->assertEquals($previous_permissions_hash, $current_permissions_hash, 'Permissions hash has reverted.');
// Ensure that the admin role doesn't have any checkboxes.
$this->drupalGet('admin/people/permissions');
foreach (array_keys($this->container->get('user.permissions')->getPermissions()) as $permission) {
$this->assertSession()->checkboxChecked('administrator[' . $permission . ']');
$this->assertSession()->fieldDisabled('administrator[' . $permission . ']');
}
}
/**
* Tests assigning of permissions for the administrator role.
*/
public function testAdministratorRole(): void {
$this->drupalLogin($this->adminUser);
$this->drupalGet('admin/people/role-settings');
// Verify that the administration role is none by default.
$this->assertTrue($this->assertSession()->optionExists('edit-user-admin-role', '')->isSelected());
$this->assertFalse(Role::load($this->rid)->isAdmin());
// Set the user's role to be the administrator role.
$edit = [];
$edit['user_admin_role'] = $this->rid;
$this->drupalGet('admin/people/role-settings');
$this->submitForm($edit, 'Save configuration');
\Drupal::entityTypeManager()->getStorage('user_role')->resetCache();
$this->assertTrue(Role::load($this->rid)->isAdmin());
// Enable block module and ensure the 'administer news feeds'
// permission is assigned by default.
\Drupal::service('module_installer')->install(['block']);
$this->assertTrue($this->adminUser->hasPermission('administer blocks'), 'The permission was automatically assigned to the administrator role');
// Ensure that selecting '- None -' removes the admin role.
$edit = [];
$edit['user_admin_role'] = '';
$this->drupalGet('admin/people/role-settings');
$this->submitForm($edit, 'Save configuration');
\Drupal::entityTypeManager()->getStorage('user_role')->resetCache();
\Drupal::configFactory()->reset();
$this->assertFalse(Role::load($this->rid)->isAdmin());
// Manually create two admin roles, in that case the single select should be
// hidden.
Role::create(['id' => 'admin_role_0', 'is_admin' => TRUE, 'label' => 'Admin role 0'])->save();
Role::create(['id' => 'admin_role_1', 'is_admin' => TRUE, 'label' => 'Admin role 1'])->save();
$this->drupalGet('admin/people/role-settings');
$this->assertSession()->fieldNotExists('user_admin_role');
}
/**
* Verify proper permission changes by user_role_change_permissions().
*/
public function testUserRoleChangePermissions(): void {
$permissions_hash_generator = $this->container->get('user_permissions_hash_generator');
$rid = $this->rid;
$account = $this->adminUser;
$previous_permissions_hash = $permissions_hash_generator->generate($account);
// Verify current permissions.
$this->assertFalse($account->hasPermission('administer users'), 'User does not have "administer users" permission.');
$this->assertTrue($account->hasPermission('access user profiles'), 'User has "access user profiles" permission.');
$this->assertTrue($account->hasPermission('administer site configuration'), 'User has "administer site configuration" permission.');
// Change permissions.
$permissions = [
'administer users' => 1,
'access user profiles' => 0,
];
user_role_change_permissions($rid, $permissions);
// Verify proper permission changes.
$this->assertTrue($account->hasPermission('administer users'), 'User now has "administer users" permission.');
$this->assertFalse($account->hasPermission('access user profiles'), 'User no longer has "access user profiles" permission.');
$this->assertTrue($account->hasPermission('administer site configuration'), 'User still has "administer site configuration" permission.');
// Verify the permissions hash has changed.
$current_permissions_hash = $permissions_hash_generator->generate($account);
$this->assertNotEquals($previous_permissions_hash, $current_permissions_hash, 'Permissions hash has changed.');
}
/**
* Verify 'access content' is listed in the correct location.
*/
public function testAccessContentPermission(): void {
$this->drupalLogin($this->adminUser);
// When Node is not installed the 'access content' permission is listed next
// to 'access site reports'.
$this->drupalGet('admin/people/permissions');
$next_row = $this->xpath('//tr[@data-drupal-selector=\'edit-permissions-access-content\']/following-sibling::tr[1]');
$this->assertEquals('edit-permissions-access-site-reports', $next_row[0]->getAttribute('data-drupal-selector'));
// When Node is installed the 'access content' permission is listed next to
// to 'view own unpublished content'.
\Drupal::service('module_installer')->install(['node']);
$this->drupalGet('admin/people/permissions');
$next_row = $this->xpath('//tr[@data-drupal-selector=\'edit-permissions-access-content\']/following-sibling::tr[1]');
$this->assertEquals('edit-permissions-view-own-unpublished-content', $next_row[0]->getAttribute('data-drupal-selector'));
}
/**
* Verify that module-specific pages have correct access.
*/
public function testAccessModulePermission(): void {
$this->drupalLogin($this->adminUser);
// When Node is not installed, the node-permissions page is not available.
$this->drupalGet('admin/people/permissions/module/node');
$this->assertSession()->statusCodeEquals(403);
// Modules that do not create permissions have no permissions pages.
\Drupal::service('module_installer')->install(['automated_cron']);
$this->drupalGet('admin/people/permissions/module/automated_cron');
$this->assertSession()->statusCodeEquals(403);
$this->drupalGet('admin/people/permissions/module/node,automated_cron');
$this->assertSession()->statusCodeEquals(403);
// When Node is installed, the node-permissions page is available.
\Drupal::service('module_installer')->install(['node']);
$this->drupalGet('admin/people/permissions/module/node');
$this->assertSession()->statusCodeEquals(200);
$this->drupalGet('admin/people/permissions/module/node,automated_cron');
$this->assertSession()->statusCodeEquals(200);
// Anonymous users cannot access any of these pages.
$this->drupalLogout();
$this->drupalGet('admin/people/permissions/module/node');
$this->assertSession()->statusCodeEquals(403);
$this->drupalGet('admin/people/permissions/module/automated_cron');
$this->assertSession()->statusCodeEquals(403);
$this->drupalGet('admin/people/permissions/module/node,automated_cron');
$this->assertSession()->statusCodeEquals(403);
}
/**
* Verify that bundle-specific pages work properly.
*/
public function testAccessBundlePermission(): void {
$this->drupalLogin($this->adminUser);
\Drupal::service('module_installer')->install(['contact', 'taxonomy']);
$this->grantPermissions(Role::load($this->rid), ['administer contact forms', 'administer taxonomy']);
// Bundles that do not have permissions have no permissions pages.
$edit = [];
$edit['label'] = 'Test contact type';
$edit['id'] = 'test_contact_type';
$edit['recipients'] = 'webmaster@example.com';
$this->drupalGet('admin/structure/contact/add');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('Contact form ' . $edit['label'] . ' has been added.');
$this->drupalGet('admin/structure/contact/manage/test_contact_type/permissions');
$this->assertSession()->statusCodeEquals(403);
// Permissions can be changed using the bundle-specific pages.
$edit = [];
$edit['name'] = 'Test vocabulary';
$edit['vid'] = 'test_vocabulary';
$this->drupalGet('admin/structure/taxonomy/add');
$this->submitForm($edit, 'Save');
$this->drupalGet('admin/structure/taxonomy/manage/test_vocabulary/overview/permissions');
$this->assertSession()->checkboxNotChecked('authenticated[create terms in test_vocabulary]');
$this->assertSession()->fieldExists('authenticated[create terms in test_vocabulary]')->check();
$this->getSession()->getPage()->pressButton('Save permissions');
$this->assertSession()->pageTextContains('The changes have been saved.');
$this->assertSession()->checkboxChecked('authenticated[create terms in test_vocabulary]');
// Typos produce 404 response, not server errors.
$this->drupalGet('admin/structure/taxonomy/manage/test_typo/overview/permissions');
$this->assertSession()->statusCodeEquals(404);
// Anonymous users cannot access any of these pages.
$this->drupalLogout();
$this->drupalGet('admin/structure/taxonomy/manage/test_vocabulary/overview/permissions');
$this->assertSession()->statusCodeEquals(403);
$this->drupalGet('admin/structure/contact/manage/test_contact_type/permissions');
$this->assertSession()->statusCodeEquals(403);
}
}

View File

@@ -0,0 +1,192 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\Core\Database\Database;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\StreamWrapper\StreamWrapperManager;
use Drupal\file\Entity\File;
use Drupal\image\Entity\ImageStyle;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\TestFileCreationTrait;
/**
* Tests user picture functionality.
*
* @group user
*/
class UserPictureTest extends BrowserTestBase {
use TestFileCreationTrait {
getTestFiles as drupalGetTestFiles;
}
use CommentTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'test_user_config',
'node',
'comment',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A regular user.
*
* @var \Drupal\user\UserInterface
*/
protected $webUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// This test expects unused managed files to be marked temporary and then
// cleaned up by file_cron().
$this->config('file.settings')
->set('make_unused_managed_files_temporary', TRUE)
->save();
$this->webUser = $this->drupalCreateUser([
'access content',
'access comments',
'post comments',
'skip comment approval',
]);
}
/**
* Tests creation, display, and deletion of user pictures.
*/
public function testCreateDeletePicture(): void {
$this->drupalLogin($this->webUser);
// Save a new picture.
$image = current($this->drupalGetTestFiles('image'));
$file = $this->saveUserPicture($image);
// Verify that the image is displayed on the user account page.
$this->drupalGet('user');
$this->assertSession()->responseContains(StreamWrapperManager::getTarget($file->getFileUri()));
// Delete the picture.
$edit = [];
$this->drupalGet('user/' . $this->webUser->id() . '/edit');
$this->submitForm($edit, 'Remove');
$this->submitForm([], 'Save');
// Call file_cron() to clean up the file. Make sure the timestamp
// of the file is older than the system.file.temporary_maximum_age
// configuration value. We use an UPDATE statement because using the API
// would set the timestamp.
Database::getConnection()->update('file_managed')
->fields([
'changed' => \Drupal::time()->getRequestTime() - ($this->config('system.file')->get('temporary_maximum_age') + 1),
])
->condition('fid', $file->id())
->execute();
\Drupal::service('cron')->run();
// Verify that the image has been deleted.
$this->assertNull(File::load($file->id()), 'File was removed from the database.');
// Clear out PHP's file stat cache so we see the current value.
clearstatcache(TRUE, $file->getFileUri());
$this->assertFileDoesNotExist($file->getFileUri());
}
/**
* Tests embedded users on node pages.
*/
public function testPictureOnNodeComment(): void {
$this->drupalLogin($this->webUser);
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
$this->addDefaultCommentField('node', 'article');
// Save a new picture.
$image = current($this->drupalGetTestFiles('image'));
$file = $this->saveUserPicture($image);
$node = $this->drupalCreateNode(['type' => 'article']);
// Enable user pictures on nodes.
$this->config('system.theme.global')->set('features.node_user_picture', TRUE)->save();
$image_style_id = $this->config('core.entity_view_display.user.user.compact')->get('content.user_picture.settings.image_style');
$style = ImageStyle::load($image_style_id);
$image_url = \Drupal::service('file_url_generator')->transformRelative($style->buildUrl($file->getFileUri()));
$alt_text = 'Profile picture for user ' . $this->webUser->getAccountName();
// Verify that the image is displayed on the node page.
$this->drupalGet('node/' . $node->id());
$elements = $this->cssSelect('article > footer img[alt="' . $alt_text . '"][src="' . $image_url . '"]');
$this->assertCount(1, $elements, 'User picture with alt text found on node page.');
// Enable user pictures on comments, instead of nodes.
$this->config('system.theme.global')
->set('features.node_user_picture', FALSE)
->set('features.comment_user_picture', TRUE)
->save();
$edit = [
'comment_body[0][value]' => $this->randomString(),
];
$this->drupalGet('comment/reply/node/' . $node->id() . '/comment');
$this->submitForm($edit, 'Save');
$elements = $this->cssSelect('#comment-1 img[alt="' . $alt_text . '"][src="' . $image_url . '"]');
$this->assertCount(1, $elements, 'User picture with alt text found on the comment.');
// Disable user pictures on comments and nodes.
$this->config('system.theme.global')
->set('features.node_user_picture', FALSE)
->set('features.comment_user_picture', FALSE)
->save();
$this->drupalGet('node/' . $node->id());
$this->assertSession()->responseNotContains(StreamWrapperManager::getTarget($file->getFileUri()));
}
/**
* Edits the user picture for the test user.
*/
public function saveUserPicture($image) {
$edit = ['files[user_picture_0]' => \Drupal::service('file_system')->realpath($image->uri)];
$this->drupalGet('user/' . $this->webUser->id() . '/edit');
$this->submitForm($edit, 'Save');
// Load actual user data from database.
$user_storage = $this->container->get('entity_type.manager')->getStorage('user');
$user_storage->resetCache([$this->webUser->id()]);
$account = $user_storage->load($this->webUser->id());
return File::load($account->user_picture->target_id);
}
/**
* Tests user picture field with a non-standard field formatter.
*
* @see user_user_view_alter()
*/
public function testUserViewAlter(): void {
\Drupal::service('module_installer')->install(['image_module_test']);
// Set dummy_image_formatter to the default view mode of user entity.
EntityViewDisplay::load('user.user.default')->setComponent('user_picture', [
'region' => 'content',
'type' => 'dummy_image_formatter',
])->save();
$this->drupalLogin($this->webUser);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('Dummy');
}
}

View File

@@ -0,0 +1,272 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Test\AssertMailTrait;
use Drupal\Core\Url;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
use Drupal\Tests\rest\Functional\ResourceTestBase;
use Drupal\user\UserInterface;
use GuzzleHttp\RequestOptions;
/**
* Tests registration of user using REST.
*
* @group user
*/
class UserRegistrationRestTest extends ResourceTestBase {
use CookieResourceTestTrait;
use AssertMailTrait {
getMails as drupalGetMails;
}
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
/**
* {@inheritdoc}
*/
protected static $resourceConfigId = 'user_registration';
/**
* {@inheritdoc}
*/
protected static $modules = ['user', 'rest'];
/**
* Entity type ID for this storage.
*
* @var string
*/
protected static string $entityTypeId;
const USER_EMAIL_DOMAIN = '@example.com';
const TEST_EMAIL_DOMAIN = 'simpletest@example.com';
/**
* {@inheritdoc}
*/
public function setUp(): void {
parent::setUp();
$auth = isset(static::$auth) ? [static::$auth] : [];
$this->provisionResource([static::$format], $auth);
$this->setUpAuthorization('POST');
}
/**
* Tests that only anonymous users can register users.
*/
public function testRegisterUser(): void {
$config = $this->config('user.settings');
// Test out different setting User Registration and Email Verification.
// Allow visitors to register with no email verification.
$config->set('register', UserInterface::REGISTER_VISITORS);
$config->set('verify_mail', 0);
$config->save();
$user = $this->registerUser('Palmer.Eldritch');
$this->assertFalse($user->isBlocked());
$this->assertNotEmpty($user->getPassword());
$email_count = count($this->drupalGetMails());
$this->assertEquals(0, $email_count);
// Attempt to register without sending a password.
$response = $this->registerRequest('PhilipK.Dick', FALSE);
$this->assertResourceErrorResponse(422, "No password provided.", $response);
// Attempt to register with a password when email verification is on.
$config->set('register', UserInterface::REGISTER_VISITORS);
$config->set('verify_mail', 1);
$config->save();
$response = $this->registerRequest('UrsulaK.LeGuin');
$this->assertResourceErrorResponse(422, 'A Password cannot be specified. It will be generated on login.', $response);
// Allow visitors to register with email verification.
$config->set('register', UserInterface::REGISTER_VISITORS);
$config->set('verify_mail', 1);
$config->save();
$name = 'Jason.Taverner';
$user = $this->registerUser($name, FALSE);
$this->assertNotEmpty($user->getPassword());
$this->assertFalse($user->isBlocked());
$this->resetAll();
$this->assertMailString('body', 'You may now log in by clicking this link', 1);
// Allow visitors to register with Admin approval and no email verification.
$config->set('register', UserInterface::REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL);
$config->set('verify_mail', 0);
$config->save();
$name = 'Alex';
$user = $this->registerUser($name);
$this->resetAll();
$this->assertNotEmpty($user->getPassword());
$this->assertTrue($user->isBlocked());
$this->assertMailString('body', 'Your application for an account is', 2);
$this->assertMailString('body', 'Alex has applied for an account', 2);
// Allow visitors to register with Admin approval and email verification.
$config->set('register', UserInterface::REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL);
$config->set('verify_mail', 1);
$config->save();
$name = 'PhilipK.Dick';
$user = $this->registerUser($name, FALSE);
$this->resetAll();
$this->assertNotEmpty($user->getPassword());
$this->assertTrue($user->isBlocked());
$this->assertMailString('body', 'Your application for an account is', 2);
$this->assertMailString('body', 'PhilipK.Dick has applied for an account', 2);
// Verify that an authenticated user cannot register a new user, despite
// being granted permission to do so because only anonymous users can
// register themselves, authenticated users with the necessary permissions
// can POST a new user to the "user" REST resource.
$this->initAuthentication();
$response = $this->registerRequest($this->account->getAccountName());
$this->assertResourceErrorResponse(403, "Only anonymous users can register a user.", $response);
}
/**
* Create the request body.
*
* @param string $name
* Name.
* @param bool $include_password
* Include Password.
* @param bool $include_email
* Include Email.
*
* @return array
* Return the request body.
*/
protected function createRequestBody($name, $include_password = TRUE, $include_email = TRUE) {
$request_body = [
'langcode' => [['value' => 'en']],
'name' => [['value' => $name]],
];
if ($include_email) {
$request_body['mail'] = [['value' => $name . self::USER_EMAIL_DOMAIN]];
}
if ($include_password) {
$request_body['pass']['value'] = 'SuperSecretPassword';
}
return $request_body;
}
/**
* Helper function to generate the request body.
*
* @param array $request_body
* The request body array.
*
* @return array
* Return the request options.
*/
protected function createRequestOptions(array $request_body) {
$request_options = $this->getAuthenticationRequestOptions('POST');
$request_options[RequestOptions::BODY] = $this->serializer->encode($request_body, static::$format);
$request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType;
return $request_options;
}
/**
* Registers a user via REST resource.
*
* @param string $name
* User name.
* @param bool $include_password
* Include the password.
* @param bool $include_email
* Include the email?
*
* @return bool|\Drupal\user\Entity\User
* Return bool or the user.
*/
protected function registerUser($name, $include_password = TRUE, $include_email = TRUE) {
// Verify that an anonymous user can register.
$response = $this->registerRequest($name, $include_password, $include_email);
$this->assertResourceResponse(200, FALSE, $response);
$user = user_load_by_name($name);
$this->assertNotEmpty($user, 'User was create as expected');
return $user;
}
/**
* Make a REST user registration request.
*
* @param string $name
* The name.
* @param bool $include_password
* Include the password?
* @param bool $include_email
* Include the email?
*
* @return \Psr\Http\Message\ResponseInterface
* Return the Response.
*/
protected function registerRequest($name, $include_password = TRUE, $include_email = TRUE) {
$user_register_url = Url::fromRoute('user.register')
->setRouteParameter('_format', static::$format);
$request_body = $this->createRequestBody($name, $include_password, $include_email);
$request_options = $this->createRequestOptions($request_body);
$response = $this->request('POST', $user_register_url, $request_options);
return $response;
}
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
switch ($method) {
case 'POST':
$this->grantPermissionsToAuthenticatedRole(['restful post user_registration']);
$this->grantPermissionsToAnonymousRole(['restful post user_registration']);
break;
default:
throw new \UnexpectedValueException();
}
}
/**
* {@inheritdoc}
*/
protected function assertNormalizationEdgeCases($method, Url $url, array $request_options): void {}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
return '';
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessCacheability() {
return new CacheableMetadata();
}
}

View File

@@ -0,0 +1,412 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\Tests\BrowserTestBase;
use Drupal\user\UserInterface;
/**
* Tests registration of user under different configurations.
*
* @group user
*/
class UserRegistrationTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['field_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
public function testRegistrationWithEmailVerification(): void {
$config = $this->config('user.settings');
// Require email verification.
$config->set('verify_mail', TRUE)->save();
// Set registration to administrator only and ensure the user registration
// page is inaccessible.
$config->set('register', UserInterface::REGISTER_ADMINISTRATORS_ONLY)->save();
$this->drupalGet('user/register');
$this->assertSession()->statusCodeEquals(403);
// Allow registration by site visitors without administrator approval.
$config->set('register', UserInterface::REGISTER_VISITORS)->save();
$edit = [];
$edit['name'] = $name = $this->randomMachineName();
$edit['mail'] = $mail = $edit['name'] . '@example.com';
$this->drupalGet('user/register');
$this->submitForm($edit, 'Create new account');
$this->assertSession()->pageTextContains('A welcome message with further instructions has been sent to your email address.');
/** @var EntityStorageInterface $storage */
$storage = $this->container->get('entity_type.manager')->getStorage('user');
$accounts = $storage->loadByProperties(['name' => $name, 'mail' => $mail]);
$new_user = reset($accounts);
$this->assertTrue($new_user->isActive(), 'New account is active after registration.');
$resetURL = user_pass_reset_url($new_user);
$this->drupalGet($resetURL);
$this->assertSession()->titleEquals('Set password | Drupal');
// Allow registration by site visitors, but require administrator approval.
$config->set('register', UserInterface::REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL)->save();
$edit = [];
$edit['name'] = $name = $this->randomMachineName();
$edit['mail'] = $mail = $edit['name'] . '@example.com';
$this->drupalGet('user/register');
$this->submitForm($edit, 'Create new account');
$this->container->get('entity_type.manager')->getStorage('user')->resetCache();
$accounts = $storage->loadByProperties(['name' => $name, 'mail' => $mail]);
$new_user = reset($accounts);
$this->assertFalse($new_user->isActive(), 'New account is blocked until approved by an administrator.');
}
public function testRegistrationWithoutEmailVerification(): void {
$config = $this->config('user.settings');
// Don't require email verification and allow registration by site visitors
// without administrator approval.
$config
->set('verify_mail', FALSE)
->set('register', UserInterface::REGISTER_VISITORS)
->save();
$edit = [];
$edit['name'] = $name = $this->randomMachineName();
$edit['mail'] = $mail = $edit['name'] . '@example.com';
// Try entering a mismatching password.
$edit['pass[pass1]'] = '99999.0';
$edit['pass[pass2]'] = '99999';
$this->drupalGet('user/register');
$this->submitForm($edit, 'Create new account');
$this->assertSession()->pageTextContains('The specified passwords do not match.');
// Enter a correct password.
$edit['pass[pass1]'] = $new_pass = $this->randomMachineName();
$edit['pass[pass2]'] = $new_pass;
$this->drupalGet('user/register');
$this->submitForm($edit, 'Create new account');
$this->container->get('entity_type.manager')->getStorage('user')->resetCache();
$accounts = $this->container->get('entity_type.manager')->getStorage('user')
->loadByProperties(['name' => $name, 'mail' => $mail]);
$new_user = reset($accounts);
$this->assertNotNull($new_user, 'New account successfully created with matching passwords.');
$this->assertSession()->pageTextContains('Registration successful. You are now logged in.');
$this->drupalLogout();
// Allow registration by site visitors, but require administrator approval.
$config->set('register', UserInterface::REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL)->save();
$edit = [];
$edit['name'] = $name = $this->randomMachineName();
$edit['mail'] = $mail = $edit['name'] . '@example.com';
$edit['pass[pass1]'] = $pass = $this->randomMachineName();
$edit['pass[pass2]'] = $pass;
$this->drupalGet('user/register');
$this->submitForm($edit, 'Create new account');
$this->assertSession()->pageTextContains('Thank you for applying for an account. Your account is currently pending approval by the site administrator.');
// Try to log in before administrator approval.
$auth = [
'name' => $name,
'pass' => $pass,
];
$this->drupalGet('user/login');
$this->submitForm($auth, 'Log in');
$this->assertSession()->pageTextContains('The username ' . $name . ' has not been activated or is blocked.');
// Activate the new account.
$accounts = $this->container->get('entity_type.manager')->getStorage('user')
->loadByProperties(['name' => $name, 'mail' => $mail]);
$new_user = reset($accounts);
$admin_user = $this->drupalCreateUser(['administer users']);
$this->drupalLogin($admin_user);
$edit = [
'status' => 1,
];
$this->drupalGet('user/' . $new_user->id() . '/edit');
$this->submitForm($edit, 'Save');
$this->drupalLogout();
// Log in after administrator approval.
$this->drupalGet('user/login');
$this->submitForm($auth, 'Log in');
$this->assertSession()->pageTextContains('Member for');
}
public function testRegistrationEmailDuplicates(): void {
// Don't require email verification and allow registration by site visitors
// without administrator approval.
$this->config('user.settings')
->set('verify_mail', FALSE)
->set('register', UserInterface::REGISTER_VISITORS)
->save();
// Set up a user to check for duplicates.
$duplicate_user = $this->drupalCreateUser();
$edit = [];
$edit['name'] = $this->randomMachineName();
$edit['mail'] = $duplicate_user->getEmail();
// Attempt to create a new account using an existing email address.
$this->drupalGet('user/register');
$this->submitForm($edit, 'Create new account');
$this->assertSession()->pageTextContains('The email address ' . $duplicate_user->getEmail() . ' is already taken.');
// Attempt to bypass duplicate email registration validation by adding spaces.
$edit['mail'] = ' ' . $duplicate_user->getEmail() . ' ';
$this->drupalGet('user/register');
$this->submitForm($edit, 'Create new account');
$this->assertSession()->pageTextContains('The email address ' . $duplicate_user->getEmail() . ' is already taken.');
}
/**
* Tests that UUID isn't cached in form state on register form.
*
* This is a regression test for https://www.drupal.org/node/2500527 to ensure
* that the form is not cached on GET requests.
*/
public function testUuidFormState(): void {
\Drupal::service('module_installer')->install(['image']);
// Add a picture field in order to ensure that no form cache is written,
// which breaks registration of more than 1 user every 6 hours.
$field_storage = FieldStorageConfig::create([
'field_name' => 'user_picture',
'entity_type' => 'user',
'type' => 'image',
]);
$field_storage->save();
$field = FieldConfig::create([
'field_name' => 'user_picture',
'entity_type' => 'user',
'bundle' => 'user',
]);
$field->save();
$form_display = EntityFormDisplay::create([
'targetEntityType' => 'user',
'bundle' => 'user',
'mode' => 'default',
'status' => TRUE,
]);
$form_display->setComponent('user_picture', [
'type' => 'image_image',
]);
$form_display->save();
// Don't require email verification and allow registration by site visitors
// without administrator approval.
$this->config('user.settings')
->set('verify_mail', FALSE)
->set('register', UserInterface::REGISTER_VISITORS)
->save();
$edit = [];
$edit['name'] = $this->randomMachineName();
$edit['mail'] = $edit['name'] . '@example.com';
$edit['pass[pass2]'] = $edit['pass[pass1]'] = $this->randomMachineName();
// Create one account.
$this->drupalGet('user/register');
$this->submitForm($edit, 'Create new account');
$this->assertSession()->statusCodeEquals(200);
$user_storage = \Drupal::entityTypeManager()->getStorage('user');
$this->assertNotEmpty($user_storage->loadByProperties(['name' => $edit['name']]));
$this->drupalLogout();
// Create a second account.
$edit['name'] = $this->randomMachineName();
$edit['mail'] = $edit['name'] . '@example.com';
$edit['pass[pass2]'] = $edit['pass[pass1]'] = $this->randomMachineName();
$this->drupalGet('user/register');
$this->submitForm($edit, 'Create new account');
$this->assertSession()->statusCodeEquals(200);
$this->assertNotEmpty($user_storage->loadByProperties(['name' => $edit['name']]));
}
public function testRegistrationDefaultValues(): void {
// Don't require email verification and allow registration by site visitors
// without administrator approval.
$config_user_settings = $this->config('user.settings')
->set('verify_mail', FALSE)
->set('register', UserInterface::REGISTER_VISITORS)
->save();
// Set the default timezone to Brussels.
$config_system_date = $this->config('system.date')
->set('timezone.user.configurable', 1)
->set('timezone.default', 'Europe/Brussels')
->save();
// Check the presence of expected cache tags.
$this->drupalGet('user/register');
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'config:user.settings');
$edit = [];
$edit['name'] = $name = $this->randomMachineName();
$edit['mail'] = $mail = $edit['name'] . '@example.com';
$edit['pass[pass1]'] = $new_pass = $this->randomMachineName();
$edit['pass[pass2]'] = $new_pass;
$this->submitForm($edit, 'Create new account');
// Check user fields.
$accounts = $this->container->get('entity_type.manager')->getStorage('user')
->loadByProperties(['name' => $name, 'mail' => $mail]);
$new_user = reset($accounts);
$this->assertEquals($name, $new_user->getAccountName(), 'Username matches.');
$this->assertEquals($mail, $new_user->getEmail(), 'Email address matches.');
// Verify that the creation time is correct.
$this->assertGreaterThan(\Drupal::time()->getRequestTime() - 20, $new_user->getCreatedTime());
$this->assertEquals($config_user_settings->get('register') == UserInterface::REGISTER_VISITORS ? 1 : 0, $new_user->isActive(), 'Correct status field.');
$this->assertEquals($config_system_date->get('timezone.default'), $new_user->getTimezone(), 'Correct time zone field.');
$this->assertEquals(\Drupal::languageManager()->getDefaultLanguage()->getId(), $new_user->langcode->value, 'Correct language field.');
$this->assertEquals(\Drupal::languageManager()->getDefaultLanguage()->getId(), $new_user->preferred_langcode->value, 'Correct preferred language field.');
$this->assertEquals($mail, $new_user->init->value, 'Correct init field.');
}
/**
* Tests username and email field constraints on user registration.
*
* @see \Drupal\user\Plugin\Validation\Constraint\UserNameUnique
* @see \Drupal\user\Plugin\Validation\Constraint\UserMailUnique
*/
public function testUniqueFields(): void {
$account = $this->drupalCreateUser();
$edit = ['mail' => 'test@example.com', 'name' => $account->getAccountName()];
$this->drupalGet('user/register');
$this->submitForm($edit, 'Create new account');
$this->assertSession()->pageTextContains("The username {$account->getAccountName()} is already taken.");
$edit = ['mail' => $account->getEmail(), 'name' => $this->randomString()];
$this->drupalGet('user/register');
$this->submitForm($edit, 'Create new account');
$this->assertSession()->pageTextContains("The email address {$account->getEmail()} is already taken.");
}
/**
* Tests Field API fields on user registration forms.
*/
public function testRegistrationWithUserFields(): void {
// Create a field on 'user' entity type.
$field_storage = FieldStorageConfig::create([
'field_name' => 'test_user_field',
'entity_type' => 'user',
'type' => 'test_field',
'cardinality' => 1,
]);
$field_storage->save();
$field = FieldConfig::create([
'field_storage' => $field_storage,
'label' => 'Some user field',
'bundle' => 'user',
'required' => TRUE,
]);
$field->save();
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
$display_repository->getFormDisplay('user', 'user')
->setComponent('test_user_field', ['type' => 'test_field_widget'])
->save();
$display_repository->getFormDisplay('user', 'user', 'register')
->save();
// Check that the field does not appear on the registration form.
$this->drupalGet('user/register');
$this->assertSession()->pageTextNotContains($field->label());
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'config:core.entity_form_display.user.user.register');
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'config:user.settings');
// Have the field appear on the registration form.
$display_repository->getFormDisplay('user', 'user', 'register')
->setComponent('test_user_field', ['type' => 'test_field_widget'])
->save();
$this->drupalGet('user/register');
$this->assertSession()->pageTextContains($field->label());
$this->assertRegistrationFormCacheTagsWithUserFields();
// Check that validation errors are correctly reported.
$edit = [];
$edit['name'] = $name = $this->randomMachineName();
$edit['mail'] = $mail = $edit['name'] . '@example.com';
// Missing input in required field.
$edit['test_user_field[0][value]'] = '';
$this->submitForm($edit, 'Create new account');
$this->assertRegistrationFormCacheTagsWithUserFields();
$this->assertSession()->pageTextContains("{$field->label()} field is required.");
// Invalid input.
$edit['test_user_field[0][value]'] = '-1';
$this->submitForm($edit, 'Create new account');
$this->assertRegistrationFormCacheTagsWithUserFields();
$this->assertSession()->pageTextContains("{$field->label()} does not accept the value -1.");
// Submit with valid data.
$value = rand(1, 255);
$edit['test_user_field[0][value]'] = $value;
$this->submitForm($edit, 'Create new account');
// Check user fields.
$accounts = $this->container->get('entity_type.manager')->getStorage('user')
->loadByProperties(['name' => $name, 'mail' => $mail]);
$new_user = reset($accounts);
$this->assertEquals($value, $new_user->test_user_field->value, 'The field value was correctly saved.');
// Check that the 'add more' button works.
$field_storage->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
$field_storage->save();
$this->drupalGet('user/register');
$this->assertRegistrationFormCacheTagsWithUserFields();
// Add two inputs.
$value = rand(1, 255);
$edit = [];
$edit['test_user_field[0][value]'] = $value;
$this->submitForm($edit, 'Add another item');
$this->submitForm($edit, 'Add another item');
// Submit with three values.
$edit['test_user_field[1][value]'] = $value + 1;
$edit['test_user_field[2][value]'] = $value + 2;
$edit['name'] = $name = $this->randomMachineName();
$edit['mail'] = $mail = $edit['name'] . '@example.com';
$this->submitForm($edit, 'Create new account');
// Check user fields.
$accounts = $this->container->get('entity_type.manager')->getStorage('user')
->loadByProperties(['name' => $name, 'mail' => $mail]);
$new_user = reset($accounts);
$this->assertEquals($value, $new_user->test_user_field[0]->value, 'The field value was correctly saved.');
$this->assertEquals($value + 1, $new_user->test_user_field[1]->value, 'The field value was correctly saved.');
$this->assertEquals($value + 2, $new_user->test_user_field[2]->value, 'The field value was correctly saved.');
}
/**
* Asserts the presence of cache tags on registration form with user fields.
*
* @internal
*/
protected function assertRegistrationFormCacheTagsWithUserFields(): void {
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'config:core.entity_form_display.user.user.register');
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'config:field.field.user.user.test_user_field');
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'config:field.storage.user.test_user_field');
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'config:user.settings');
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the requirements checks of the User module.
*
* @group user
*/
class UserRequirementsTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests that the requirements check can detect a missing anonymous user.
*/
public function testAnonymousUser(): void {
// Remove the anonymous user.
\Drupal::database()
->delete('users')
->condition('uid', 0)
->execute();
$this->drupalLogin($this->drupalCreateUser([
'access administration pages',
'administer site configuration',
]));
$this->drupalGet('/admin/reports/status');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains("The anonymous user does not exist.");
}
}

View File

@@ -0,0 +1,147 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
/**
* Tests adding, editing and deleting user roles and changing role weights.
*
* @group user
*/
class UserRoleAdminTest extends BrowserTestBase {
/**
* User with admin privileges.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* Modules to enable.
*
* @var string[]
*/
protected static $modules = ['block'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->adminUser = $this->drupalCreateUser([
'administer permissions',
'administer users',
]);
$this->drupalPlaceBlock('local_tasks_block', ['id' => 'test_role_admin_test_local_tasks_block']);
}
/**
* Tests adding, renaming and deleting roles.
*/
public function testRoleAdministration(): void {
$this->drupalLogin($this->adminUser);
$default_langcode = \Drupal::languageManager()->getDefaultLanguage()->getId();
// Test presence of tab.
$this->drupalGet('admin/people/permissions');
$this->assertSession()->elementsCount('xpath', '//div[@id="block-test-role-admin-test-local-tasks-block"]/ul/li/a[contains(., "Roles")]', 1);
// Test adding a role. (In doing so, we use a role name that happens to
// correspond to an integer, to test that the role administration pages
// correctly distinguish between role names and IDs.)
$role_name = '123';
$edit = ['label' => $role_name, 'id' => $role_name];
$this->drupalGet('admin/people/roles/add');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("Role 123 has been added.");
$role = Role::load($role_name);
$this->assertIsObject($role);
// Check that the role was created in site default language.
$this->assertEquals($default_langcode, $role->language()->getId());
// Verify permissions local task can be accessed when editing a role.
$this->drupalGet("admin/people/roles/manage/{$role->id()}");
$local_tasks_block = $this->assertSession()->elementExists('css', '#block-test-role-admin-test-local-tasks-block');
$local_tasks_block->clickLink('Permissions');
$this->assertSession()->fieldExists("{$role->id()}[change own username]");
// Try adding a duplicate role.
$this->drupalGet('admin/people/roles/add');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("The machine-readable name is already in use. It must be unique.");
// Test renaming a role.
$role_name = '456';
$edit = ['label' => $role_name];
$this->drupalGet("admin/people/roles/manage/{$role->id()}");
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("Role {$role_name} has been updated.");
\Drupal::entityTypeManager()->getStorage('user_role')->resetCache([$role->id()]);
$new_role = Role::load($role->id());
$this->assertEquals($role_name, $new_role->label(), 'The role name has been successfully changed.');
// Test deleting a role.
$this->drupalGet("admin/people/roles/manage/{$role->id()}");
$this->clickLink('Delete');
$this->submitForm([], 'Delete');
$this->assertSession()->pageTextContains("Role {$role_name} has been deleted.");
$this->assertSession()->linkByHrefNotExists("admin/people/roles/manage/{$role->id()}", 'Role edit link removed.');
\Drupal::entityTypeManager()->getStorage('user_role')->resetCache([$role->id()]);
$this->assertNull(Role::load($role->id()), 'A deleted role can no longer be loaded.');
// Make sure that the system-defined roles can be edited via the user
// interface.
$this->drupalGet('admin/people/roles/manage/' . RoleInterface::ANONYMOUS_ID);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextNotContains('Delete role');
$this->drupalGet('admin/people/roles/manage/' . RoleInterface::AUTHENTICATED_ID);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextNotContains('Delete role');
}
/**
* Tests user role weight change operation and ordering.
*/
public function testRoleWeightOrdering(): void {
$this->drupalLogin($this->adminUser);
$roles = Role::loadMultiple();
$weight = count($roles);
$new_role_weights = [];
$saved_rids = [];
// Change the role weights to make the roles in reverse order.
$edit = [];
foreach ($roles as $role) {
$edit['entities[' . $role->id() . '][weight]'] = $weight;
$new_role_weights[$role->id()] = $weight;
$saved_rids[] = $role->id();
$weight--;
}
$this->drupalGet('admin/people/roles');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('The role settings have been updated.');
// Load up the user roles with the new weights.
$roles = Role::loadMultiple();
$rids = [];
// Test that the role weights have been correctly saved.
foreach ($roles as $role) {
$this->assertEquals($role->getWeight(), $new_role_weights[$role->id()]);
$rids[] = $role->id();
}
// The order of the roles should be reversed.
$this->assertSame(array_reverse($saved_rids), $rids);
}
}

View File

@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests that users can be assigned and unassigned roles.
*
* @group user
*/
class UserRolesAssignmentTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$admin_user = $this->drupalCreateUser([
'administer permissions',
'administer users',
]);
$this->drupalLogin($admin_user);
}
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Test that user can be assigned role and that the role can be removed again.
*/
public function testAssignAndRemoveRole(): void {
$rid = $this->drupalCreateRole(['administer users']);
$account = $this->drupalCreateUser();
// Assign the role to the user.
$this->drupalGet('user/' . $account->id() . '/edit');
$this->submitForm(["roles[{$rid}]" => $rid], 'Save');
$this->assertSession()->pageTextContains('The changes have been saved.');
$this->assertSession()->checkboxChecked('edit-roles-' . $rid);
$this->userLoadAndCheckRoleAssigned($account, $rid);
// Remove the role from the user.
$this->drupalGet('user/' . $account->id() . '/edit');
$this->submitForm(["roles[{$rid}]" => FALSE], 'Save');
$this->assertSession()->pageTextContains('The changes have been saved.');
$this->assertSession()->checkboxNotChecked('edit-roles-' . $rid);
$this->userLoadAndCheckRoleAssigned($account, $rid, FALSE);
}
/**
* Tests assigning a role at user creation and removing the role.
*/
public function testCreateUserWithRole(): void {
$rid = $this->drupalCreateRole(['administer users']);
// Create a new user and add the role at the same time.
$edit = [
'name' => $this->randomMachineName(),
'mail' => $this->randomMachineName() . '@example.com',
'pass[pass1]' => $pass = $this->randomString(),
'pass[pass2]' => $pass,
"roles[$rid]" => $rid,
];
$this->drupalGet('admin/people/create');
$this->submitForm($edit, 'Create new account');
$this->assertSession()->pageTextContains('Created a new user account for ' . $edit['name'] . '.');
// Get the newly added user.
$account = user_load_by_name($edit['name']);
$this->drupalGet('user/' . $account->id() . '/edit');
$this->assertSession()->checkboxChecked('edit-roles-' . $rid);
$this->userLoadAndCheckRoleAssigned($account, $rid);
// Remove the role again.
$this->drupalGet('user/' . $account->id() . '/edit');
$this->submitForm(["roles[{$rid}]" => FALSE], 'Save');
$this->assertSession()->pageTextContains('The changes have been saved.');
$this->assertSession()->checkboxNotChecked('edit-roles-' . $rid);
$this->userLoadAndCheckRoleAssigned($account, $rid, FALSE);
}
/**
* Check role on user object.
*
* @param object $account
* The user account to check.
* @param string $rid
* The role ID to search for.
* @param bool $is_assigned
* (optional) Whether to assert that $rid exists (TRUE) or not (FALSE).
* Defaults to TRUE.
*/
private function userLoadAndCheckRoleAssigned($account, $rid, $is_assigned = TRUE) {
$user_storage = $this->container->get('entity_type.manager')->getStorage('user');
$user_storage->resetCache([$account->id()]);
$account = $user_storage->load($account->id());
if ($is_assigned) {
$this->assertContains($rid, $account->getRoles());
}
else {
$this->assertNotContains($rid, $account->getRoles());
}
}
}

View File

@@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Verifies that sensitive information is hidden from unauthorized users.
*
* @group user
*/
class UserSearchTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['search'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
public function testUserSearch(): void {
// Verify that a user without 'administer users' permission cannot search
// for users by email address. Additionally, ensure that the username has a
// plus sign to ensure searching works with that.
$user1 = $this->drupalCreateUser([
'access user profiles',
'search content',
], "foo+bar");
$this->drupalLogin($user1);
$keys = $user1->getEmail();
$edit = ['keys' => $keys];
$this->drupalGet('search/user');
$this->submitForm($edit, 'Search');
$this->assertSession()->pageTextContains('Your search yielded no results.');
$this->assertSession()->pageTextContains('no results');
// Verify that a non-matching query gives an appropriate message.
$keys = 'nomatch';
$edit = ['keys' => $keys];
$this->drupalGet('search/user');
$this->submitForm($edit, 'Search');
$this->assertSession()->pageTextContains('no results');
// Verify that a user with search permission can search for users by name.
$keys = $user1->getAccountName();
$edit = ['keys' => $keys];
$this->drupalGet('search/user');
$this->submitForm($edit, 'Search');
$this->assertSession()->linkExists($keys, 0, 'Search by username worked for non-admin user');
// Verify that searching by sub-string works too.
$subkey = substr($keys, 1, 5);
$edit = ['keys' => $subkey];
$this->drupalGet('search/user');
$this->submitForm($edit, 'Search');
$this->assertSession()->linkExists($keys, 0, 'Search by username substring worked for non-admin user');
// Verify that wildcard search works.
$subkey = substr($keys, 0, 2) . '*' . substr($keys, 4, 2);
$edit = ['keys' => $subkey];
$this->drupalGet('search/user');
$this->submitForm($edit, 'Search');
$this->assertSession()->linkExists($keys, 0, 'Search with wildcard worked for non-admin user');
// Verify that a user with 'administer users' permission can search by
// email.
$user2 = $this->drupalCreateUser([
'administer users',
'access user profiles',
'search content',
]);
$this->drupalLogin($user2);
$keys = $user2->getEmail();
$edit = ['keys' => $keys];
$this->drupalGet('search/user');
$this->submitForm($edit, 'Search');
$this->assertSession()->pageTextContains($keys);
$this->assertSession()->pageTextContains($user2->getAccountName());
// Verify that a substring works too for email.
$subkey = substr($keys, 1, 5);
$edit = ['keys' => $subkey];
$this->drupalGet('search/user');
$this->submitForm($edit, 'Search');
$this->assertSession()->pageTextContains($keys);
$this->assertSession()->pageTextContains($user2->getAccountName());
// Verify that wildcard search works for email
$subkey = substr($keys, 0, 2) . '*' . substr($keys, 4, 2);
$edit = ['keys' => $subkey];
$this->drupalGet('search/user');
$this->submitForm($edit, 'Search');
$this->assertSession()->pageTextContains($user2->getAccountName());
// Verify that if they search by user name, they see email address too.
$keys = $user1->getAccountName();
$edit = ['keys' => $keys];
$this->drupalGet('search/user');
$this->submitForm($edit, 'Search');
$this->assertSession()->pageTextContains($keys);
$this->assertSession()->pageTextContains($user1->getEmail());
// Create a blocked user.
$blocked_user = $this->drupalCreateUser();
$blocked_user->block();
$blocked_user->save();
// Verify that users with "administer users" permissions can see blocked
// accounts in search results.
$edit = ['keys' => $blocked_user->getAccountName()];
$this->drupalGet('search/user');
$this->submitForm($edit, 'Search');
$this->assertSession()->pageTextContains($blocked_user->getAccountName());
// Verify that users without "administer users" permissions do not see
// blocked accounts in search results.
$this->drupalLogin($user1);
$edit = ['keys' => $blocked_user->getAccountName()];
$this->drupalGet('search/user');
$this->submitForm($edit, 'Search');
$this->assertSession()->pageTextContains('Your search yielded no results.');
// Ensure that a user without access to user profiles cannot access the
// user search page.
$user3 = $this->drupalCreateUser(['search content']);
$this->drupalLogin($user3);
$this->drupalGet('search/user');
$this->assertSession()->statusCodeEquals(403);
// Ensure that a user without search permission cannot access the user
// search page.
$user4 = $this->drupalCreateUser(['access user profiles']);
$this->drupalLogin($user4);
$this->drupalGet('search/user');
$this->assertSession()->statusCodeEquals(403);
$this->drupalLogout();
}
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Test 'sub-admin' account with permission to edit some users but without 'administer users' permission.
*
* @group user
*/
class UserSubAdminTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['user_access_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests create and cancel forms as 'sub-admin'.
*/
public function testSubAdmin(): void {
$user = $this->drupalCreateUser(['sub-admin']);
$this->drupalLogin($user);
// Test that the create user page has admin fields.
$this->drupalGet('admin/people/create');
$this->assertSession()->fieldExists("edit-name");
$this->assertSession()->fieldExists("edit-notify");
// Not 'status' or 'roles' as they require extra permission.
$this->assertSession()->fieldNotExists("edit-status-0");
$this->assertSession()->fieldNotExists("edit-role");
// Test that create user gives an admin style message.
$edit = [
'name' => $this->randomMachineName(),
'mail' => $this->randomMachineName() . '@example.com',
'pass[pass1]' => $pass = $this->randomString(),
'pass[pass2]' => $pass,
'notify' => FALSE,
];
$this->drupalGet('admin/people/create');
$this->submitForm($edit, 'Create new account');
$this->assertSession()->pageTextContains('Created a new user account for ' . $edit['name'] . '. No email has been sent.');
// Test that the cancel user page has admin fields.
$cancel_user = $this->createUser();
$this->drupalGet('user/' . $cancel_user->id() . '/cancel');
$this->assertSession()->responseContains('Are you sure you want to cancel the account ' . $cancel_user->getAccountName() . '?');
$this->assertSession()->responseContains('Disable the account and keep its content.');
// Test that cancel confirmation gives an admin style message.
$this->submitForm([], 'Confirm');
$this->assertSession()->pageTextContains('Account ' . $cancel_user->getAccountName() . ' has been disabled.');
// Repeat with permission to select account cancellation method.
$user
->addRole($this->drupalCreateRole(['select account cancellation method']))
->save();
$cancel_user = $this->createUser();
$this->drupalGet('user/' . $cancel_user->id() . '/cancel');
$this->assertSession()->pageTextContains('Cancellation method');
}
}

View File

@@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\Core\Datetime\Entity\DateFormat;
use Drupal\Tests\BrowserTestBase;
/**
* Set a user time zone and verify that dates are displayed in local time.
*
* @group user
*/
class UserTimeZoneTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['node', 'system_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests the display of dates and time when user-configurable time zones are set.
*/
public function testUserTimeZone(): void {
// Setup date/time settings for Los Angeles time.
$this->config('system.date')
->set('timezone.user.configurable', 1)
->set('timezone.default', 'America/Los_Angeles')
->save();
// Load the 'medium' date format, which is the default for node creation
// time, and override it. Since we are testing time zones with Daylight
// Saving Time, and need to future proof against changes to the zoneinfo
// database, we choose the 'I' format placeholder instead of a
// human-readable zone name. With 'I', a 1 means the date is in DST, and 0
// if not.
DateFormat::load('medium')
->setPattern('Y-m-d H:i I')
->save();
// Create a user account and login.
$web_user = $this->drupalCreateUser();
$this->drupalLogin($web_user);
// Create some nodes with different authored-on dates.
// Two dates in PST (winter time):
$date1 = '2007-03-09 21:00:00 -0800';
$date2 = '2007-03-11 01:00:00 -0800';
// One date in PDT (summer time):
$date3 = '2007-03-20 21:00:00 -0700';
$this->drupalCreateContentType(['type' => 'article']);
$node1 = $this->drupalCreateNode(['created' => strtotime($date1), 'type' => 'article']);
$node2 = $this->drupalCreateNode(['created' => strtotime($date2), 'type' => 'article']);
$node3 = $this->drupalCreateNode(['created' => strtotime($date3), 'type' => 'article']);
// Confirm date format and time zone.
$this->drupalGet('node/' . $node1->id());
// Date should be PST.
$this->assertSession()->pageTextContains('2007-03-09 21:00 0');
$this->drupalGet('node/' . $node2->id());
// Date should be PST.
$this->assertSession()->pageTextContains('2007-03-11 01:00 0');
$this->drupalGet('node/' . $node3->id());
// Date should be PST.
$this->assertSession()->pageTextContains('2007-03-20 21:00 1');
// Change user time zone to Santiago time.
$edit = [];
$edit['mail'] = $web_user->getEmail();
$edit['timezone'] = 'America/Santiago';
$this->drupalGet("user/" . $web_user->id() . "/edit");
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('The changes have been saved.');
// Confirm date format and time zone.
$this->drupalGet('node/' . $node1->id());
// Date should be Chile summer time, five hours ahead of PST.
$this->assertSession()->pageTextContains('2007-03-10 02:00 1');
$this->drupalGet('node/' . $node2->id());
// Date should be Chile time, four hours ahead of PST.
$this->assertSession()->pageTextContains('2007-03-11 05:00 0');
$this->drupalGet('node/' . $node3->id());
// Date should be Chile time, three hours ahead of PDT.
$this->assertSession()->pageTextContains('2007-03-21 00:00 0');
// Ensure that anonymous users also use the default timezone.
$this->drupalLogout();
$this->drupalGet('node/' . $node1->id());
// Date should be PST.
$this->assertSession()->pageTextContains('2007-03-09 21:00 0');
$this->drupalGet('node/' . $node2->id());
// Date should be PST.
$this->assertSession()->pageTextContains('2007-03-11 01:00 0');
$this->drupalGet('node/' . $node3->id());
// Date should be PDT.
$this->assertSession()->pageTextContains('2007-03-20 21:00 1');
// Format a date without accessing the current user at all and
// ensure that it uses the default timezone.
$this->drupalGet('/system-test/date');
// Date should be PST.
$this->assertSession()->pageTextContains('2016-01-13 08:29 0');
}
}

View File

@@ -0,0 +1,177 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\Core\Url;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\BrowserTestBase;
use Drupal\user\Entity\User;
/**
* Tests the replacement of user tokens.
*
* @group user
*/
class UserTokenReplaceTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['language', 'user_hooks_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
ConfigurableLanguage::createFromLangcode('de')->save();
}
/**
* Creates a user, then tests the tokens generated from it.
*/
public function testUserTokenReplacement(): void {
$token_service = \Drupal::token();
$language_interface = \Drupal::languageManager()->getCurrentLanguage();
$url_options = [
'absolute' => TRUE,
'language' => $language_interface,
];
\Drupal::state()->set('user_hooks_test_user_format_name_alter', TRUE);
\Drupal::state()->set('user_hooks_test_user_format_name_alter_safe', TRUE);
// Create two users and log them in one after another.
$user1 = $this->drupalCreateUser([]);
$user2 = $this->drupalCreateUser([]);
$this->drupalLogin($user1);
$this->drupalLogout();
$this->drupalLogin($user2);
$account = User::load($user1->id());
$global_account = User::load(\Drupal::currentUser()->id());
/** @var \Drupal\Core\Datetime\DateFormatterInterface $date_formatter */
$date_formatter = $this->container->get('date.formatter');
// Generate and test tokens.
$tests = [];
$tests['[user:uid]'] = $account->id();
$tests['[user:name]'] = $account->getAccountName();
$tests['[user:account-name]'] = $account->getAccountName();
$tests['[user:display-name]'] = $account->getDisplayName();
$tests['[user:mail]'] = $account->getEmail();
$tests['[user:url]'] = $account->toUrl('canonical', $url_options)->toString();
$tests['[user:edit-url]'] = $account->toUrl('edit-form', $url_options)->toString();
$tests['[user:last-login]'] = $date_formatter->format($account->getLastLoginTime(), 'medium', '', NULL, $language_interface->getId());
$tests['[user:last-login:short]'] = $date_formatter->format($account->getLastLoginTime(), 'short', '', NULL, $language_interface->getId());
$tests['[user:created]'] = $date_formatter->format($account->getCreatedTime(), 'medium', '', NULL, $language_interface->getId());
$tests['[user:created:short]'] = $date_formatter->format($account->getCreatedTime(), 'short', '', NULL, $language_interface->getId());
$tests['[current-user:name]'] = $global_account->getAccountName();
$tests['[current-user:account-name]'] = $global_account->getAccountName();
$tests['[current-user:display-name]'] = $global_account->getDisplayName();
$base_bubbleable_metadata = BubbleableMetadata::createFromObject($account);
$metadata_tests = [];
$metadata_tests['[user:uid]'] = $base_bubbleable_metadata;
$metadata_tests['[user:name]'] = $base_bubbleable_metadata;
$metadata_tests['[user:account-name]'] = $base_bubbleable_metadata;
$metadata_tests['[user:display-name]'] = $base_bubbleable_metadata;
$metadata_tests['[user:mail]'] = $base_bubbleable_metadata;
$metadata_tests['[user:url]'] = $base_bubbleable_metadata;
$metadata_tests['[user:edit-url]'] = $base_bubbleable_metadata;
$bubbleable_metadata = clone $base_bubbleable_metadata;
// This test runs with the Language module enabled, which means config is
// overridden by LanguageConfigFactoryOverride (to provide translations of
// config). This causes the interface language cache context to be added for
// config entities. The four next tokens use DateFormat Config entities, and
// therefore have the interface language cache context.
$bubbleable_metadata->addCacheContexts(['languages:language_interface']);
$metadata_tests['[user:last-login]'] = $bubbleable_metadata->addCacheTags(['rendered']);
$metadata_tests['[user:last-login:short]'] = $bubbleable_metadata;
$metadata_tests['[user:created]'] = $bubbleable_metadata;
$metadata_tests['[user:created:short]'] = $bubbleable_metadata;
$metadata_tests['[current-user:name]'] = $base_bubbleable_metadata->merge(BubbleableMetadata::createFromObject($global_account)->addCacheContexts(['user']));
$metadata_tests['[current-user:account-name]'] = $base_bubbleable_metadata->merge(BubbleableMetadata::createFromObject($global_account)->addCacheContexts(['user']));
$metadata_tests['[current-user:display-name]'] = $base_bubbleable_metadata->merge(BubbleableMetadata::createFromObject($global_account)->addCacheContexts(['user']));
// Test to make sure that we generated something for each token.
$this->assertNotContains(0, array_map('strlen', $tests), 'No empty tokens generated.');
foreach ($tests as $input => $expected) {
$bubbleable_metadata = new BubbleableMetadata();
$output = $token_service->replace($input, ['user' => $account], ['langcode' => $language_interface->getId()], $bubbleable_metadata);
$this->assertSame((string) $expected, (string) $output, "Failed test case: {$input}");
$this->assertEquals($metadata_tests[$input], $bubbleable_metadata);
}
// Generate tokens for the anonymous user.
$anonymous_user = User::load(0);
$tests = [];
$tests['[user:uid]'] = 'not yet assigned';
$tests['[user:display-name]'] = $anonymous_user->getDisplayName();
$base_bubbleable_metadata = BubbleableMetadata::createFromObject($anonymous_user);
$metadata_tests = [];
$metadata_tests['[user:uid]'] = $base_bubbleable_metadata;
$bubbleable_metadata = clone $base_bubbleable_metadata;
$bubbleable_metadata->addCacheableDependency(\Drupal::config('user.settings'));
$metadata_tests['[user:display-name]'] = $bubbleable_metadata;
foreach ($tests as $input => $expected) {
$bubbleable_metadata = new BubbleableMetadata();
$output = $token_service->replace($input, ['user' => $anonymous_user], ['langcode' => $language_interface->getId()], $bubbleable_metadata);
$this->assertSame((string) $expected, (string) $output, "Failed test case: {$input}");
$this->assertEquals($metadata_tests[$input], $bubbleable_metadata);
}
// Generate login and cancel link.
$tests = [];
$tests['[user:one-time-login-url]'] = user_pass_reset_url($account);
$tests['[user:cancel-url]'] = user_cancel_url($account);
// Generate tokens with interface language.
$link = Url::fromRoute('user.page', [], ['absolute' => TRUE])->toString();
foreach ($tests as $input => $expected) {
$output = $token_service->replace($input, ['user' => $account], ['langcode' => $language_interface->getId(), 'callback' => 'user_mail_tokens', 'clear' => TRUE]);
$this->assertStringStartsWith($link, $output, 'Generated URL is in interface language.');
}
// Generate tokens with the user's preferred language.
$account->preferred_langcode = 'de';
$account->save();
$link = Url::fromRoute('user.page', [], ['language' => \Drupal::languageManager()->getLanguage($account->getPreferredLangcode()), 'absolute' => TRUE])->toString();
foreach ($tests as $input => $expected) {
$output = $token_service->replace($input, ['user' => $account], ['callback' => 'user_mail_tokens', 'clear' => TRUE]);
$this->assertStringStartsWith($link, $output, "Generated URL is in the user's preferred language.");
}
// Generate tokens with one specific language.
$link = Url::fromRoute('user.page', [], ['language' => \Drupal::languageManager()->getLanguage('de'), 'absolute' => TRUE])->toString();
foreach ($tests as $input => $expected) {
foreach ([$user1, $user2] as $account) {
$output = $token_service->replace($input, ['user' => $account], ['langcode' => 'de', 'callback' => 'user_mail_tokens', 'clear' => TRUE]);
$this->assertStringStartsWith($link, $output, "Generated URL in the requested language.");
}
}
// Generate user display name tokens when safe markup is returned.
// @see user_hooks_test_user_format_name_alter()
\Drupal::state()->set('user_hooks_test_user_format_name_alter_safe', TRUE);
$input = '[user:display-name] [current-user:display-name]';
$expected = "<em>{$user1->id()}</em> <em>{$user2->id()}</em>";
$output = $token_service->replace($input, ['user' => $user1]);
$this->assertSame($expected, (string) $output);
}
}

View File

@@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional;
use Drupal\Tests\content_translation\Functional\ContentTranslationUITestBase;
/**
* Tests the User Translation UI.
*
* @group user
*/
class UserTranslationUITest extends ContentTranslationUITestBase {
/**
* The user name of the test user.
*
* @var string
*/
protected $name;
/**
* {@inheritdoc}
*/
protected $defaultCacheContexts = [
'languages:language_interface',
'theme',
'url.query_args:_wrapper_format',
'user.permissions',
'url.site',
];
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'language',
'content_translation',
'user',
'views',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
$this->entityTypeId = 'user';
$this->testLanguageSelector = FALSE;
$this->name = $this->randomMachineName();
parent::setUp();
$this->doSetup();
\Drupal::entityTypeManager()->getStorage('user')->resetCache();
}
/**
* {@inheritdoc}
*/
protected function getTranslatorPermissions() {
return array_merge(parent::getTranslatorPermissions(), ['administer users']);
}
/**
* {@inheritdoc}
*/
protected function getNewEntityValues($langcode) {
// User name is not translatable hence we use a fixed value.
return ['name' => $this->name] + parent::getNewEntityValues($langcode);
}
/**
* {@inheritdoc}
*/
protected function doTestTranslationEdit() {
$storage = $this->container->get('entity_type.manager')
->getStorage($this->entityTypeId);
$storage->resetCache([$this->entityId]);
$entity = $storage->load($this->entityId);
$languages = $this->container->get('language_manager')->getLanguages();
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("{$entity->getTranslation($langcode)->label()} [{$languages[$langcode]->getName()} translation]");
}
}
}
/**
* Tests translated user deletion.
*/
public function testTranslatedUserDeletion(): void {
$this->drupalLogin($this->administrator);
$entity_id = $this->createEntity($this->getNewEntityValues('en'), 'en');
$entity = $this->container->get('entity_type.manager')
->getStorage($this->entityTypeId)
->load($entity_id);
$translated_entity = $entity->addTranslation('fr');
$translated_entity->save();
$url = $entity->toUrl(
'edit-form',
['language' => $this->container->get('language_manager')->getLanguage('en')]
);
$this->drupalGet($url);
$this->clickLink('Cancel account');
$this->assertSession()->statusCodeEquals(200);
}
}

View File

@@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional\Views;
use Drupal\Core\Cache\Cache;
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
use Drupal\user\Plugin\views\access\Role;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\Views;
/**
* Tests views role access plugin.
*
* @group user
* @see \Drupal\user\Plugin\views\access\Role
*/
class AccessRoleTest extends AccessTestBase {
use AssertPageCacheContextsAndTagsTrait;
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_access_role'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE, $modules = ['user_test_views']): void {
parent::setUp($import_test_views, $modules);
}
/**
* Tests role access plugin.
*/
public function testAccessRole(): void {
/** @var \Drupal\views\ViewEntityInterface $view */
$view = \Drupal::entityTypeManager()->getStorage('view')->load('test_access_role');
$display = &$view->getDisplay('default');
$display['display_options']['access']['options']['role'] = [
$this->normalRole => $this->normalRole,
];
$view->save();
$this->container->get('router.builder')->rebuildIfNeeded();
$expected = [
'config' => ['user.role.' . $this->normalRole],
'module' => ['user', 'views_test_data'],
];
$this->assertSame($expected, $view->calculateDependencies()->getDependencies());
$executable = Views::executableFactory()->get($view);
$executable->setDisplay('page_1');
$access_plugin = $executable->display_handler->getPlugin('access');
$this->assertInstanceOf(Role::class, $access_plugin);
// Test the access() method on the access plugin.
$this->assertFalse($executable->display_handler->access($this->webUser));
$this->assertTrue($executable->display_handler->access($this->normalUser));
$this->drupalLogin($this->webUser);
$this->drupalGet('test-role');
$this->assertSession()->statusCodeEquals(403);
$this->assertCacheContext('user.roles');
$this->drupalLogin($this->normalUser);
$this->drupalGet('test-role');
$this->assertSession()->statusCodeEquals(200);
$this->assertCacheContext('user.roles');
// Test allowing multiple roles.
$view = Views::getView('test_access_role')->storage;
$display = &$view->getDisplay('default');
$display['display_options']['access']['options']['role'] = [
$this->normalRole => $this->normalRole,
'anonymous' => 'anonymous',
];
$view->save();
$this->container->get('router.builder')->rebuildIfNeeded();
// Ensure that the list of roles is sorted correctly, if the generated role
// ID comes before 'anonymous', see https://www.drupal.org/node/2398259.
$roles = ['user.role.anonymous', 'user.role.' . $this->normalRole];
sort($roles);
$expected = [
'config' => $roles,
'module' => ['user', 'views_test_data'],
];
$this->assertSame($expected, $view->calculateDependencies()->getDependencies());
$this->drupalLogin($this->webUser);
$this->drupalGet('test-role');
$this->assertSession()->statusCodeEquals(403);
$this->assertCacheContext('user.roles');
$this->drupalLogout();
$this->drupalGet('test-role');
$this->assertSession()->statusCodeEquals(200);
$this->assertCacheContext('user.roles');
$this->drupalLogin($this->normalUser);
$this->drupalGet('test-role');
$this->assertSession()->statusCodeEquals(200);
$this->assertCacheContext('user.roles');
}
/**
* Tests access on render caching.
*/
public function testRenderCaching(): void {
$view = Views::getView('test_access_role');
$display = &$view->storage->getDisplay('default');
$display['display_options']['cache'] = [
'type' => 'tag',
];
$display['display_options']['access']['options']['role'] = [
$this->normalRole => $this->normalRole,
];
$view->save();
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
/** @var \Drupal\Core\Session\AccountSwitcherInterface $account_switcher */
$account_switcher = \Drupal::service('account_switcher');
// First access as user with access.
$build = DisplayPluginBase::buildBasicRenderable('test_access_role', 'default');
$account_switcher->switchTo($this->normalUser);
$result = $renderer->renderInIsolation($build);
$this->assertContains('user.roles', $build['#cache']['contexts']);
$this->assertEquals(['config:views.view.test_access_role'], $build['#cache']['tags']);
$this->assertEquals(Cache::PERMANENT, $build['#cache']['max-age']);
$this->assertNotSame('', $result);
// Then without access.
$build = DisplayPluginBase::buildBasicRenderable('test_access_role', 'default');
$account_switcher->switchTo($this->webUser);
$result = $renderer->renderInIsolation($build);
// @todo Fix this in https://www.drupal.org/node/2551037,
// DisplayPluginBase::applyDisplayCacheabilityMetadata() is not invoked when
// using buildBasicRenderable() and a Views access plugin returns FALSE.
// $this->assertContains('user.roles', $build['#cache']['contexts']);
// $this->assertEquals([], $build['#cache']['tags']);
$this->assertEquals(Cache::PERMANENT, $build['#cache']['max-age']);
$this->assertEquals('', $result);
}
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional\Views;
/**
* A common test base class for the user access plugin tests.
*/
abstract class AccessTestBase extends UserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['block'];
/**
* Contains a user object that has no special permissions.
*
* @var \Drupal\user\UserInterface
*/
protected $webUser;
/**
* Contains a user object that has the 'views_test_data test permission'.
*
* @var \Drupal\user\UserInterface
*/
protected $normalUser;
/**
* Contains a role ID that is used by the webUser.
*
* @var string
*/
protected $webRole;
/**
* Contains a role ID that is used by the normalUser.
*
* @var string
*/
protected $normalRole;
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE, $modules = []): void {
parent::setUp($import_test_views, $modules);
$this->drupalPlaceBlock('system_breadcrumb_block');
$this->enableViewsTestModule();
$this->webUser = $this->drupalCreateUser();
$roles = $this->webUser->getRoles();
$this->webRole = $roles[0];
$this->normalRole = $this->drupalCreateRole([]);
$this->normalUser = $this->drupalCreateUser([
'views_test_data test permission',
]);
$this->normalUser->addRole($this->normalRole)->save();
// @todo when all the plugin information is cached make a reset function and
// call it here.
}
}

View File

@@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional\Views;
use Drupal\user\Entity\User;
/**
* Tests if entity access is respected on a user bulk form.
*
* @group user
* @see \Drupal\user\Plugin\views\field\UserBulkForm
* @see \Drupal\user\Tests\Views\BulkFormTest
*/
class BulkFormAccessTest extends UserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['user_access_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_user_bulk_form'];
/**
* Tests if users that may not be edited, can not be edited in bulk.
*/
public function testUserEditAccess(): void {
// Create an authenticated user.
$no_edit_user = $this->drupalCreateUser([], 'no_edit');
// Ensure this account is not blocked.
$this->assertFalse($no_edit_user->isBlocked(), 'The user is not blocked.');
// Log in as user admin.
$admin_user = $this->drupalCreateUser(['administer users']);
$this->drupalLogin($admin_user);
// Ensure that the account "no_edit" can not be edited.
$this->drupalGet('user/' . $no_edit_user->id() . '/edit');
$this->assertFalse($no_edit_user->access('update', $admin_user));
$this->assertSession()->statusCodeEquals(403);
// Test blocking the account "no_edit".
$edit = [
'user_bulk_form[' . ($no_edit_user->id() - 1) . ']' => TRUE,
'action' => 'user_block_user_action',
];
$this->drupalGet('test-user-bulk-form');
$this->submitForm($edit, 'Apply to selected items');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains("No access to execute Block the selected user(s) on the User {$no_edit_user->label()}.");
// Re-load the account "no_edit" and ensure it is not blocked.
$no_edit_user = User::load($no_edit_user->id());
$this->assertFalse($no_edit_user->isBlocked(), 'The user is not blocked.');
// Create a normal user which can be edited by the admin user
$normal_user = $this->drupalCreateUser();
$this->assertTrue($normal_user->access('update', $admin_user));
$edit = [
'user_bulk_form[' . ($normal_user->id() - 1) . ']' => TRUE,
'action' => 'user_block_user_action',
];
$this->drupalGet('test-user-bulk-form');
$this->submitForm($edit, 'Apply to selected items');
$normal_user = User::load($normal_user->id());
$this->assertTrue($normal_user->isBlocked(), 'The user is blocked.');
// Log in as user without the 'administer users' permission.
$this->drupalLogin($this->drupalCreateUser());
$edit = [
'user_bulk_form[' . ($normal_user->id() - 1) . ']' => TRUE,
'action' => 'user_unblock_user_action',
];
$this->drupalGet('test-user-bulk-form');
$this->submitForm($edit, 'Apply to selected items');
// Re-load the normal user and ensure it is still blocked.
$normal_user = User::load($normal_user->id());
$this->assertTrue($normal_user->isBlocked(), 'The user is still blocked.');
}
/**
* Tests if users that may not be deleted, can not be deleted in bulk.
*/
public function testUserDeleteAccess(): void {
// Create two authenticated users.
$account = $this->drupalCreateUser([], 'no_delete');
$account2 = $this->drupalCreateUser([], 'may_delete');
// Log in as user admin.
$this->drupalLogin($this->drupalCreateUser(['administer users']));
// Ensure that the account "no_delete" can not be deleted.
$this->drupalGet('user/' . $account->id() . '/cancel');
$this->assertSession()->statusCodeEquals(403);
// Ensure that the account "may_delete" *can* be deleted.
$this->drupalGet('user/' . $account2->id() . '/cancel');
$this->assertSession()->statusCodeEquals(200);
// Test deleting the accounts "no_delete" and "may_delete".
$edit = [
'user_bulk_form[' . ($account->id() - 1) . ']' => TRUE,
'user_bulk_form[' . ($account2->id() - 1) . ']' => TRUE,
'action' => 'user_cancel_user_action',
];
$this->drupalGet('test-user-bulk-form');
$this->submitForm($edit, 'Apply to selected items');
$edit = [
'user_cancel_method' => 'user_cancel_delete',
];
$this->submitForm($edit, 'Confirm');
// Ensure the account "no_delete" still exists.
$account = User::load($account->id());
$this->assertNotNull($account, 'The user "no_delete" is not deleted.');
// Ensure the account "may_delete" no longer exists.
$account = User::load($account2->id());
$this->assertNull($account, 'The user "may_delete" is deleted.');
}
}

View File

@@ -0,0 +1,155 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional\Views;
use Drupal\user\Entity\Role;
use Drupal\user\Entity\User;
use Drupal\user\RoleInterface;
use Drupal\views\Views;
/**
* Tests a user bulk form.
*
* @group user
* @see \Drupal\user\Plugin\views\field\UserBulkForm
*/
class BulkFormTest extends UserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['views_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_user_bulk_form', 'test_user_bulk_form_combine_filter'];
/**
* Tests the user bulk form.
*/
public function testBulkForm(): void {
// Log in as a user without 'administer users'.
$this->drupalLogin($this->drupalCreateUser(['administer permissions']));
$user_storage = $this->container->get('entity_type.manager')->getStorage('user');
// Create a user which actually can change users.
$this->drupalLogin($this->drupalCreateUser(['administer users']));
$this->drupalGet('test-user-bulk-form');
$this->assertNotEmpty($this->cssSelect('#edit-action option'));
// Test submitting the page with no selection.
$edit = [
'action' => 'user_block_user_action',
];
$this->drupalGet('test-user-bulk-form');
$this->submitForm($edit, 'Apply to selected items');
$this->assertSession()->pageTextContains('No users selected.');
// Assign a role to a user.
$account = $user_storage->load($this->users[0]->id());
$roles = Role::loadMultiple();
unset($roles[RoleInterface::ANONYMOUS_ID]);
unset($roles[RoleInterface::AUTHENTICATED_ID]);
$role = key($roles);
$this->assertFalse($account->hasRole($role), 'The user currently does not have a custom role.');
$edit = [
'user_bulk_form[1]' => TRUE,
'action' => 'user_add_role_action.' . $role,
];
$this->submitForm($edit, 'Apply to selected items');
// Re-load the user and check their roles.
$user_storage->resetCache([$account->id()]);
$account = $user_storage->load($account->id());
$this->assertTrue($account->hasRole($role), 'The user now has the custom role.');
$edit = [
'user_bulk_form[1]' => TRUE,
'action' => 'user_remove_role_action.' . $role,
];
$this->submitForm($edit, 'Apply to selected items');
// Re-load the user and check their roles.
$user_storage->resetCache([$account->id()]);
$account = $user_storage->load($account->id());
$this->assertFalse($account->hasRole($role), 'The user no longer has the custom role.');
// Block a user using the bulk form.
$this->assertTrue($account->isActive(), 'The user is not blocked.');
$this->assertSession()->pageTextContains($account->label());
$edit = [
'user_bulk_form[1]' => TRUE,
'action' => 'user_block_user_action',
];
$this->submitForm($edit, 'Apply to selected items');
// Re-load the user and check their status.
$user_storage->resetCache([$account->id()]);
$account = $user_storage->load($account->id());
$this->assertTrue($account->isBlocked(), 'The user is blocked.');
$this->assertSession()->pageTextNotContains($account->label());
// Remove the user status filter from the view.
$view = Views::getView('test_user_bulk_form');
$view->removeHandler('default', 'filter', 'status');
$view->storage->save();
// Ensure the anonymous user is found.
$this->drupalGet('test-user-bulk-form');
$this->assertSession()->pageTextContains($this->config('user.settings')->get('anonymous'));
// Attempt to block the anonymous user.
$edit = [
'user_bulk_form[0]' => TRUE,
'action' => 'user_block_user_action',
];
$this->submitForm($edit, 'Apply to selected items');
$anonymous_account = $user_storage->load(0);
$this->assertTrue($anonymous_account->isBlocked(), 'Ensure the anonymous user got blocked.');
// Test the list of available actions with a value that contains a dot.
$this->drupalLogin($this->drupalCreateUser([
'administer permissions',
'administer views',
'administer users',
]));
$action_id = 'user_add_role_action.' . $role;
$edit = [
'options[include_exclude]' => 'exclude',
"options[selected_actions][$action_id]" => $action_id,
];
$this->drupalGet('admin/structure/views/nojs/handler/test_user_bulk_form/default/field/user_bulk_form');
$this->submitForm($edit, 'Apply');
$this->submitForm([], 'Save');
$this->drupalGet('test-user-bulk-form');
$this->assertSession()->optionNotExists('edit-action', $action_id);
$edit['options[include_exclude]'] = 'include';
$this->drupalGet('admin/structure/views/nojs/handler/test_user_bulk_form/default/field/user_bulk_form');
$this->submitForm($edit, 'Apply');
$this->submitForm([], 'Save');
$this->drupalGet('test-user-bulk-form');
$this->assertSession()->optionExists('edit-action', $action_id);
}
/**
* Tests the user bulk form with a combined field filter on the bulk column.
*/
public function testBulkFormCombineFilter(): void {
// Add a user.
User::load($this->users[0]->id());
$view = Views::getView('test_user_bulk_form_combine_filter');
$errors = $view->validate();
$this->assertEquals(sprintf('Field User: Bulk update set in Global: Combine fields filter is not usable for this filter type. Combined field filter only works for simple fields.'), reset($errors['default']));
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional\Views;
use Drupal\Tests\views\Functional\ViewTestBase;
/**
* Tests the permission field handler ui.
*
* @group user
* @see \Drupal\user\Plugin\views\filter\Permissions
*/
class FilterPermissionUiTest extends ViewTestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_filter_permission'];
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['user', 'user_test_views', 'views_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE, $modules = ['user_test_views']): void {
parent::setUp($import_test_views, $modules);
$this->enableViewsTestModule();
}
/**
* Tests basic filter handler settings in the UI.
*/
public function testHandlerUI(): void {
$this->drupalLogin($this->drupalCreateUser([
'administer views',
'administer users',
]));
$this->drupalGet('admin/structure/views/view/test_filter_permission/edit/default');
// Verify that the handler summary is correctly displaying the selected
// permission.
$this->assertSession()->linkExists('User: Permission (= View user information)');
$this->submitForm([], 'Save');
// Verify that we can save the view.
$this->assertSession()->pageTextNotContains('No valid values found on filter: User: Permission.');
$this->assertSession()->pageTextContains('The view test_filter_permission has been saved.');
// Verify that the handler summary is also correct when multiple values are
// selected in the filter.
$edit = [
'options[value][]' => [
'access user profiles',
'administer views',
],
];
$this->drupalGet('admin/structure/views/nojs/handler/test_filter_permission/default/filter/permission');
$this->submitForm($edit, 'Apply');
$this->assertSession()->linkExists('User: Permission (or View us…)');
$this->submitForm([], 'Save');
// Verify that we can save the view.
$this->assertSession()->pageTextNotContains('No valid values found on filter: User: Permission.');
$this->assertSession()->pageTextContains('The view test_filter_permission has been saved.');
}
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional\Views;
use Drupal\Component\Utility\Html;
use Drupal\user\Entity\User;
/**
* Tests the handler of the user: role field.
*
* @group user
* @see views_handler_field_user_name
*/
class HandlerFieldRoleTest extends UserTestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_views_handler_field_role'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
public function testRole(): void {
// Create a couple of roles for the view.
$role_name_a = 'a' . $this->randomMachineName(8);
$this->drupalCreateRole(['access content'], $role_name_a, '<em>' . $role_name_a . '</em>', 9);
$role_name_b = 'b' . $this->randomMachineName(8);
$this->drupalCreateRole(['access content'], $role_name_b, $role_name_b, 8);
$role_name_not_assigned = $this->randomMachineName(8);
$this->drupalCreateRole(['access content'], $role_name_not_assigned, $role_name_not_assigned);
// Add roles to user 1.
$user = User::load(1);
$user->addRole($role_name_a)->addRole($role_name_b)->save();
$this->drupalLogin($this->createUser(['access user profiles']));
$this->drupalGet('/test-views-handler-field-role');
// Verify that the view test_views_handler_field_role renders role assigned
// to user in the correct order and markup in role names is escaped.
$this->assertSession()->responseContains($role_name_b . Html::escape('<em>' . $role_name_a . '</em>'));
// Verify that the view test_views_handler_field_role does not render a role
// not assigned to a user.
$this->assertSession()->pageTextNotContains($role_name_not_assigned);
}
}

View File

@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional\Views;
use Drupal\Core\Render\RenderContext;
use Drupal\views\Views;
/**
* Tests the handler of the user: name field.
*
* @group user
* @see views_handler_field_user_name
*/
class HandlerFieldUserNameTest extends UserTestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_views_handler_field_user_name'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
public function testUserName(): void {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$new_user = $this->drupalCreateUser(['access user profiles']);
$this->drupalLogin($new_user);
// Set defaults.
$view = Views::getView('test_views_handler_field_user_name');
$view->initHandlers();
$view->field['name']->options['link_to_user'] = TRUE;
$view->field['name']->options['type'] = 'user_name';
$view->field['name']->init($view, $view->getDisplay('default'));
$view->field['name']->options['id'] = 'name';
$this->executeView($view);
$anon_name = $this->config('user.settings')->get('anonymous');
$view->result[0]->_entity->setUsername('');
$view->result[0]->_entity->uid->value = 0;
$render = (string) $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertStringContainsString($anon_name, $render, 'For user 0 it should use the default anonymous name by default.');
$render = (string) $renderer->executeInRenderContext(new RenderContext(), function () use ($view, $new_user) {
return $view->field['name']->advancedRender($view->result[$new_user->id()]);
});
$this->assertStringContainsString($new_user->getDisplayName(), $render, 'If link to user is checked the username should be part of the output.');
$this->assertStringContainsString('user/' . $new_user->id(), $render, 'If link to user is checked the link to the user should appear as well.');
$view->field['name']->options['link_to_user'] = FALSE;
$view->field['name']->options['type'] = 'string';
$render = (string) $renderer->executeInRenderContext(new RenderContext(), function () use ($view, $new_user) {
return $view->field['name']->advancedRender($view->result[$new_user->id()]);
});
$this->assertEquals($new_user->getDisplayName(), $render, 'If the user is not linked the username should be printed out for a normal user.');
}
/**
* Tests that the field handler works when no additional fields are added.
*/
public function testNoAdditionalFields(): void {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$view = Views::getView('test_views_handler_field_user_name');
$this->executeView($view);
$username = $this->randomMachineName();
$view->result[0]->_entity->setUsername($username);
$view->result[0]->_entity->uid->value = 1;
$render = (string) $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
return $view->field['name']->advancedRender($view->result[0]);
});
$this->assertStringContainsString($username, $render, 'If link to user is checked the username should be part of the output.');
}
}

View File

@@ -0,0 +1,196 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional\Views;
use Drupal\views\Views;
use Drupal\Tests\views\Functional\ViewTestBase;
/**
* Tests the handler of the user: name filter.
*
* @group user
* @see Views\user\Plugin\views\filter\Name
*/
class HandlerFilterUserNameTest extends ViewTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['views_ui', 'user_test_views'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_user_name'];
/**
* Accounts used by this test.
*
* @var array
*/
protected $accounts = [];
/**
* Usernames of $accounts.
*
* @var array
*/
protected $names = [];
/**
* Stores the column map for this testCase.
*
* @var array
*/
public $columnMap = [
'uid' => 'uid',
];
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE, $modules = ['user_test_views']): void {
parent::setUp($import_test_views, $modules);
$this->enableViewsTestModule();
$this->accounts = [];
$this->names = [];
for ($i = 0; $i < 3; $i++) {
$this->accounts[] = $account = $this->drupalCreateUser();
$this->names[] = $account->label();
}
}
/**
* Tests just using the filter.
*/
public function testUserNameApi(): void {
$view = Views::getView('test_user_name');
$view->initHandlers();
$view->filter['uid']->value = [$this->accounts[0]->id()];
$this->executeView($view);
$this->assertIdenticalResultset($view, [['uid' => $this->accounts[0]->id()]], $this->columnMap);
$this->assertNull($view->filter['uid']->getValueOptions());
}
/**
* Tests using the user interface.
*/
public function testAdminUserInterface(): void {
$admin_user = $this->drupalCreateUser([
'administer views',
'administer site configuration',
]);
$this->drupalLogin($admin_user);
$path = 'admin/structure/views/nojs/handler/test_user_name/default/filter/uid';
$this->drupalGet($path);
// Pass in an invalid username, the validation should catch it.
$users = [$this->randomMachineName()];
$users = array_map('strtolower', $users);
$edit = [
'options[value]' => implode(', ', $users),
];
$this->drupalGet($path);
$this->submitForm($edit, 'Apply');
$this->assertSession()->pageTextContains('There are no users matching "' . implode(', ', $users) . '".');
// Pass in an invalid username and a valid username.
$random_name = $this->randomMachineName();
$users = [$random_name, $this->names[0]];
$users = array_map('strtolower', $users);
$edit = [
'options[value]' => implode(', ', $users),
];
$users = [$users[0]];
$this->drupalGet($path);
$this->submitForm($edit, 'Apply');
$this->assertSession()->pageTextContains('There are no users matching "' . implode(', ', $users) . '".');
// Pass in just valid usernames.
$users = $this->names;
$users = array_map('strtolower', $users);
$edit = [
'options[value]' => implode(', ', $users),
];
$this->drupalGet($path);
$this->submitForm($edit, 'Apply');
$this->assertSession()->pageTextNotContains('There are no users matching "' . implode(', ', $users) . '".');
}
/**
* Tests exposed filters.
*/
public function testExposedFilter(): void {
$path = 'test_user_name';
$options = [];
// Pass in an invalid username, the validation should catch it.
$users = [$this->randomMachineName()];
$users = array_map('strtolower', $users);
$options['query']['uid'] = implode(', ', $users);
$this->drupalGet($path, $options);
$this->assertSession()->pageTextContains('There are no users matching "' . implode(', ', $users) . '".');
// Pass in an invalid target_id in for the entity_autocomplete value format.
// There should be no errors, but all results should be returned as the
// default value for the autocomplete will not match any users so should
// be empty.
$options['query']['uid'] = [['target_id' => 9999]];
$this->drupalGet($path, $options);
// The actual result should contain all of the user ids.
foreach ($this->accounts as $account) {
$this->assertSession()->pageTextContains($account->id());
}
// Pass in an invalid username and a valid username.
$users = [$this->randomMachineName(), $this->names[0]];
$users = array_map('strtolower', $users);
$options['query']['uid'] = implode(', ', $users);
$users = [$users[0]];
$this->drupalGet($path, $options);
$this->assertSession()->pageTextContains('There are no users matching "' . implode(', ', $users) . '".');
// Pass in just valid usernames.
$users = $this->names;
$options['query']['uid'] = implode(', ', $users);
$this->drupalGet($path, $options);
$this->assertSession()->pageTextNotContains('Unable to find user');
// The actual result should contain all of the user ids.
foreach ($this->accounts as $account) {
$this->assertSession()->pageTextContains($account->id());
}
// Pass in just valid user IDs in the entity_autocomplete target_id format.
$options['query']['uid'] = array_map(function ($account) {
return ['target_id' => $account->id()];
}, $this->accounts);
$this->drupalGet($path, $options);
$this->assertSession()->pageTextNotContains('Unable to find user');
// The actual result should contain all of the user ids.
foreach ($this->accounts as $account) {
$this->assertSession()->pageTextContains($account->id());
}
}
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional\Views;
/**
* Tests the handler of the user: roles argument.
*
* @group user
* @see \Drupal\user\Plugin\views\argument\RolesRid
*/
class RolesRidArgumentTest extends UserTestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_user_roles_rid'];
/**
* {@inheritdoc}
*/
protected static $modules = ['views_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests the generated title of a user: roles argument.
*/
public function testArgumentTitle(): void {
$role_id = $this->createRole([], 'markup_role_name', '<em>Role name with markup</em>');
$this->createRole([], 'second_role_name', 'Second role name');
$user = $this->createUser([], 'User with role one');
$user->addRole($role_id)->save();
$second_user = $this->createUser([], 'User with role two');
$second_user->addRole('second_role_name')->save();
$this->drupalGet('/user_roles_rid_test/markup_role_name');
$this->assertSession()->assertEscaped('<em>Role name with markup</em>');
$views_user = $this->drupalCreateUser(['administer views']);
$this->drupalLogin($views_user);
// Change the View to allow multiple values for the roles.
$edit = [
'options[break_phrase]' => TRUE,
];
$this->drupalGet('admin/structure/views/nojs/handler/test_user_roles_rid/page_1/argument/roles_target_id');
$this->submitForm($edit, 'Apply');
$this->submitForm([], 'Save');
$this->drupalGet('/user_roles_rid_test/markup_role_name+second_role_name');
$this->assertSession()->pageTextContains('User with role one');
$this->assertSession()->pageTextContains('User with role two');
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional\Views;
use Drupal\Tests\views\Functional\ViewTestBase;
/**
* Tests the changed field.
*
* @group user
*/
class UserChangedTest extends ViewTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['views_ui', 'user_test_views'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_user_changed'];
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE, $modules = ['user_test_views']): void {
parent::setUp($import_test_views, $modules);
$this->enableViewsTestModule();
}
/**
* Tests changed field.
*/
public function testChangedField(): void {
$path = 'test_user_changed';
$options = [];
$this->drupalGet($path, $options);
$this->assertSession()->pageTextContains('Updated date: ' . date('Y-m-d', \Drupal::time()->getRequestTime()));
}
}

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional\Views;
/**
* Checks changing entity and field access.
*
* @group user
*/
class UserFieldsAccessChangeTest extends UserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['user_access_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_user_fields_access'];
/**
* Tests if another module can change field access.
*/
public function testUserFieldAccess(): void {
$this->drupalGet('test_user_fields_access');
// User has access to name and created date by default.
$this->assertSession()->pageTextContains('Name');
$this->assertSession()->pageTextContains('Created');
// User does not by default have access to init, mail and status.
$this->assertSession()->pageTextNotContains('Init');
$this->assertSession()->pageTextNotContains('Email');
$this->assertSession()->pageTextNotContains('Status');
// Assign sub-admin role to grant extra access.
$user = $this->drupalCreateUser(['sub-admin']);
$this->drupalLogin($user);
$this->drupalGet('test_user_fields_access');
// Access for init, mail and status is added in hook_entity_field_access().
$this->assertSession()->pageTextContains('Init');
$this->assertSession()->pageTextContains('Email');
$this->assertSession()->pageTextContains('Status');
}
/**
* Test user name link.
*
* Tests that the user name formatter shows a link to the user when there is
* access but not otherwise.
*/
public function testUserNameLink(): void {
$test_user = $this->drupalCreateUser();
$xpath = "//td/a[.='" . $test_user->getAccountName() . "']/@href[.='" . $test_user->toUrl()->toString() . "']";
$attributes = [
'title' => 'View user profile.',
];
$link = $test_user->toLink(NULL, 'canonical', ['attributes' => $attributes])->toString();
// No access, so no link.
$this->drupalGet('test_user_fields_access');
$this->assertSession()->pageTextContains($test_user->getAccountName());
$this->assertSession()->elementNotExists('xpath', $xpath);
// Assign sub-admin role to grant extra access.
$user = $this->drupalCreateUser(['sub-admin']);
$this->drupalLogin($user);
$this->drupalGet('test_user_fields_access');
$this->assertSession()->elementsCount('xpath', $xpath, 1);
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Functional\Views;
use Drupal\Tests\views\Functional\ViewTestBase;
use Drupal\user\Entity\User;
abstract class UserTestBase extends ViewTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['user_test_views', 'node'];
/**
* Users to use during this test.
*
* @var array
*/
protected $users = [];
/**
* Nodes to use during this test.
*
* @var array
*/
protected $nodes = [];
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE, $modules = ['user_test_views']): void {
parent::setUp($import_test_views, $modules);
$this->users[] = $this->drupalCreateUser();
$this->users[] = User::load(1);
$this->nodes[] = $this->drupalCreateNode(['uid' => $this->users[0]->id()]);
$this->nodes[] = $this->drupalCreateNode(['uid' => 1]);
}
}

View File

@@ -0,0 +1,181 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests the JS components added to the PasswordConfirm render element.
*
* @group user
*/
class PasswordConfirmWidgetTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* WebAssert object.
*
* @var \Drupal\Tests\WebAssert
*/
protected $assert;
/**
* User for testing.
*
* @var \Drupal\user\UserInterface
*/
protected $testUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->assert = $this->assertSession();
// Create a user.
$this->testUser = $this->createUser();
$this->drupalLogin($this->testUser);
}
/**
* Tests the components added to the password confirm widget.
*/
public function testPasswordConfirmWidgetJsComponents(): void {
$this->drupalGet($this->testUser->toUrl('edit-form'));
$password_confirm_widget_selector = '.js-form-type-password-confirm.js-form-item-pass';
$password_parent_selector = '.js-form-item-pass-pass1';
$password_confirm_selector = '.js-form-item-pass-pass2';
$password_confirm_widget = $this->assert->elementExists('css', $password_confirm_widget_selector);
$password_parent_item = $password_confirm_widget->find('css', $password_parent_selector);
$password_confirm_item = $password_confirm_widget->find('css', $password_confirm_selector);
// Check that 'password-parent' and 'confirm-parent' are added to the
// appropriate elements.
$this->assertNotNull($this->assert->waitForElement('css', "$password_parent_selector.password-parent"));
$this->assertTrue($password_parent_item->hasClass('password-parent'));
$this->assertNotNull($this->assert->waitForElement('css', "$password_confirm_selector.confirm-parent"));
$this->assertTrue($password_confirm_item->hasClass('confirm-parent'));
// Check the elements of the main password item.
$this->assertTrue($password_parent_item->has('css', 'input.js-password-field'));
// Strength meter and bar.
$this->assertTrue($password_parent_item->has('css', 'input.js-password-field + .password-strength > [data-drupal-selector="password-strength-meter"]:first-child [data-drupal-selector="password-strength-indicator"]'));
// Password strength feedback. No strength text feedback present without
// input.
$this->assertTrue($password_parent_item->has('css', '.password-strength > [data-drupal-selector="password-strength-meter"] + .password-strength__title:last-child > [data-drupal-selector="password-strength-text"]'));
$this->assertEmpty($password_parent_item->find('css', '.password-strength > [data-drupal-selector="password-strength-meter"] + .password-strength__title:last-child > [data-drupal-selector="password-strength-text"]')->getText());
// Check the elements of the password confirm item.
$this->assertTrue($password_confirm_item->has('css', 'input.js-password-confirm'));
// Check the password suggestions element.
$this->assertTrue($password_confirm_item->has('css', "$password_confirm_selector + .password-suggestions"));
$this->assertFalse($password_confirm_item->has('css', "$password_confirm_selector + .password-suggestions > ul > li"));
$this->assertFalse($password_confirm_item->find('css', "$password_confirm_selector + .password-suggestions")->isVisible());
$this->assertTrue($password_confirm_item->find('css', "$password_confirm_selector + .password-suggestions")->getHtml() === '');
// Fill only the main input for first.
$this->drupalGet($this->testUser->toUrl('edit-form'));
// Wait for the JS.
$this->assert->waitForElement('css', "$password_parent_selector.password-parent");
// Fill main input.
$password_confirm_widget->fillField('Password', 'o');
// Password tips should be refreshed and get visible.
$this->assertNotNull($this->assert->waitForElement('css', "$password_confirm_selector + .password-suggestions > ul > li"));
$this->assertTrue($password_confirm_item->find('css', "$password_confirm_selector + .password-suggestions > ul > li")->isVisible());
// Password match message must become invisible.
$this->assertFalse($password_confirm_item->find('css', 'input.js-password-confirm + [data-drupal-selector="password-confirm-message"]')->isVisible());
// Password strength message should be updated.
$this->assert->elementContains('css', "$password_confirm_widget_selector $password_parent_selector", '<div aria-live="polite" aria-atomic="true" class="password-strength__title">Password strength: <span class="password-strength__text" data-drupal-selector="password-strength-text">Weak</span></div>');
// Deleting the input must not change the element above.
$password_confirm_widget->fillField('Password', 'o');
$this->assertFalse($password_confirm_item->find('css', 'input.js-password-confirm + [data-drupal-selector="password-confirm-message"]')->isVisible());
$this->assertTrue($password_confirm_item->find('css', "$password_confirm_selector + .password-suggestions > ul > li")->isVisible());
$this->assert->elementContains('css', "$password_confirm_widget_selector $password_parent_selector", '<div aria-live="polite" aria-atomic="true" class="password-strength__title">Password strength: <span class="password-strength__text" data-drupal-selector="password-strength-text">Weak</span></div>');
// Now fill both the main and confirm input.
$password_confirm_widget->fillField('Password', 'oooooooooO0∘');
$password_confirm_widget->fillField('Confirm password', 'oooooooooO0∘');
// Bar should be 100% wide.
$this->assert->elementAttributeContains('css', 'input.js-password-field + .password-strength > [data-drupal-selector="password-strength-meter"] [data-drupal-selector="password-strength-indicator"]', 'style', 'width: 100%;');
$this->assert->elementTextContains('css', "$password_confirm_widget_selector $password_parent_selector [data-drupal-selector='password-strength-text']", 'Strong');
// Password match message must be visible.
$this->assertTrue($password_confirm_item->find('css', 'input.js-password-confirm + [data-drupal-selector="password-confirm-message"]')->isVisible());
$this->assertTrue($password_confirm_item->find('css', 'input.js-password-confirm + [data-drupal-selector="password-confirm-message"] > [data-drupal-selector="password-match-status-text"]')->hasClass('ok'));
$this->assert->elementTextContains('css', 'input.js-password-confirm + [data-drupal-selector="password-confirm-message"] > [data-drupal-selector="password-match-status-text"]', 'yes');
// Password suggestions should get invisible.
$this->assertFalse($password_confirm_item->find('css', "$password_confirm_selector + .password-suggestions")->isVisible());
}
/**
* Ensures that password match message is visible when widget is initialized.
*/
public function testPasswordConfirmMessage(): void {
$this->drupalGet($this->testUser->toUrl('edit-form'));
$password_confirm_widget_selector = '.js-form-type-password-confirm.js-form-item-pass';
$password_confirm_selector = '.js-form-item-pass-pass2';
$password_confirm_widget = $this->assert->elementExists('css', $password_confirm_widget_selector);
$password_confirm_item = $password_confirm_widget->find('css', $password_confirm_selector);
// Password match message.
$this->assertTrue($password_confirm_item->has('css', 'input.js-password-confirm + [data-drupal-selector="password-confirm-message"]'));
$this->assertTrue($password_confirm_item->find('css', 'input.js-password-confirm + [data-drupal-selector="password-confirm-message"]')->isVisible());
$this->assert->elementContains('css', "$password_confirm_widget_selector $password_confirm_selector", '<div aria-live="polite" aria-atomic="true" class="password-confirm-message" data-drupal-selector="password-confirm-message">Passwords match: <span data-drupal-selector="password-match-status-text"></span></div>');
}
/**
* Tests the password confirm widget so that only confirm input is filled.
*/
public function testFillConfirmOnly(): void {
$this->drupalGet($this->testUser->toUrl('edit-form'));
$password_confirm_widget_selector = '.js-form-type-password-confirm.js-form-item-pass';
$password_parent_selector = '.js-form-item-pass-pass1';
$password_confirm_selector = '.js-form-item-pass-pass2';
$password_confirm_widget = $this->assert->elementExists('css', $password_confirm_widget_selector);
$password_confirm_item = $password_confirm_widget->find('css', $password_confirm_selector);
$password_parent_item = $password_confirm_widget->find('css', $password_parent_selector);
// Fill only the confirm input.
$password_confirm_widget->fillField('Confirm password', 'o');
// Password tips should be refreshed and get visible.
$this->assertNotNull($this->assert->waitForElement('css', "$password_confirm_selector + .password-suggestions > ul > li"));
$this->assertTrue($password_confirm_item->find('css', "$password_confirm_selector + .password-suggestions")->isVisible());
// The appropriate strength text should appear.
$this->assert->elementContains('css', "$password_confirm_widget_selector $password_parent_selector", '<div aria-live="polite" aria-atomic="true" class="password-strength__title">Password strength: <span class="password-strength__text" data-drupal-selector="password-strength-text">Weak</span></div>');
// Password match.
$this->assertTrue($password_confirm_item->find('css', 'input.js-password-confirm + [data-drupal-selector="password-confirm-message"]')->isVisible());
$this->assert->elementContains('css', "$password_confirm_widget_selector $password_confirm_selector [data-drupal-selector='password-confirm-message']", 'Passwords match: <span data-drupal-selector="password-match-status-text" class="error">no</span>');
// Deleting the input should hide the 'password match', but password
// strength and tips must remain visible.
$password_confirm_widget->fillField('Confirm password', '');
$this->assertFalse($password_confirm_item->find('css', 'input.js-password-confirm + [data-drupal-selector="password-confirm-message"]')->isVisible());
$this->assert->elementContains('css', "$password_confirm_widget_selector $password_confirm_selector [data-drupal-selector='password-confirm-message']", 'Passwords match: <span data-drupal-selector="password-match-status-text" class="error">no</span>');
$this->assertTrue($password_confirm_item->find('css', "$password_confirm_selector + .password-suggestions")->isVisible());
$this->assertTrue($password_parent_item->find('css', '.password-strength > .password-strength__meter + .password-strength__title')->isVisible());
$this->assert->elementContains('css', "$password_confirm_widget_selector $password_parent_selector", '<div aria-live="polite" aria-atomic="true" class="password-strength__title">Password strength: <span class="password-strength__text" data-drupal-selector="password-strength-text">Weak</span></div>');
}
}

View File

@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests the JavaScript functionality of the permission filter.
*
* @group user
*/
class PermissionFilterTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['user', 'system'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$admin_user = $this->drupalCreateUser([
'administer permissions',
]);
$this->drupalLogin($admin_user);
}
/**
* Tests that filter results announcement has correct pluralization.
*/
public function testPermissionFilter(): void {
// Find the permission filter field.
$this->drupalGet('admin/people/permissions');
$assertSession = $this->assertSession();
$session = $this->getSession();
$page = $session->getPage();
$filter = $page->findField('edit-text');
// Get all permission rows, for assertions later.
$permission_rows = $page->findAll('css', 'tbody tr td .permission');
// Administer filter reduces the number of visible rows.
$filter->setValue('Administer');
$session->wait(1000, "jQuery('tr[data-drupal-selector=\"edit-permissions-access-content\"]').length == 0");
$visible_rows = $this->filterVisibleElements($permission_rows);
// Test Drupal.announce() message when multiple matches are expected.
$expected_message = count($visible_rows) . ' permissions are available in the modified list.';
$assertSession->elementTextContains('css', '#drupal-live-announce', $expected_message);
self::assertGreaterThan(count($visible_rows), count($permission_rows));
self::assertGreaterThan(1, count($visible_rows));
// Test Drupal.announce() message when one match is expected.
// Using a very specific permission name, we expect only one row.
$filter->setValue('Administer site configuration');
$session->wait(1000, "jQuery('tr[data-drupal-selector=\"edit-permissions-access-content\"]').length == 0");
$visible_rows = $this->filterVisibleElements($permission_rows);
self::assertEquals(1, count($visible_rows));
$expected_message = '1 permission is available in the modified list.';
$assertSession->elementTextContains('css', '#drupal-live-announce', $expected_message);
// Test Drupal.announce() message when no matches are expected.
$filter->setValue('Pan-Galactic Gargle Blaster');
$session->wait(1000, "jQuery('tr[data-drupal-selector=\"edit-permissions-access-content\"]').length == 0");
$visible_rows = $this->filterVisibleElements($permission_rows);
self::assertEquals(0, count($visible_rows));
$expected_message = '0 permissions are available in the modified list.';
$assertSession->elementTextContains('css', '#drupal-live-announce', $expected_message);
}
/**
* Removes any non-visible elements from the passed array.
*
* @param \Behat\Mink\Element\NodeElement[] $elements
* An array of node elements.
*
* @return \Behat\Mink\Element\NodeElement[]
* An array of node elements.
*/
protected function filterVisibleElements(array $elements): array {
$elements = array_filter($elements, function ($element) {
return $element->isVisible();
});
return $elements;
}
}

View File

@@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
/**
* Tests user registration forms with additional fields.
*
* @group user
*/
class RegistrationWithUserFieldsTest extends WebDriverTestBase {
/**
* WebAssert object.
*
* @var \Drupal\Tests\WebAssert
*/
protected $webAssert;
/**
* DocumentElement object.
*
* @var \Behat\Mink\Element\DocumentElement
*/
protected $page;
/**
* {@inheritdoc}
*/
protected static $modules = ['field_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->page = $this->getSession()->getPage();
$this->webAssert = $this->assertSession();
}
/**
* Tests Field API fields on user registration forms.
*/
public function testRegistrationWithUserFields(): void {
// Create a field on 'user' entity type.
$field_storage = FieldStorageConfig::create([
'field_name' => 'test_user_field',
'entity_type' => 'user',
'type' => 'test_field',
'cardinality' => 1,
]);
$field_storage->save();
$field = FieldConfig::create([
'field_storage' => $field_storage,
'label' => 'Some user field',
'bundle' => 'user',
'required' => TRUE,
]);
$field->save();
\Drupal::service('entity_display.repository')->getFormDisplay('user', 'user', 'default')
->setComponent('test_user_field', ['type' => 'test_field_widget'])
->save();
$user_registration_form = \Drupal::service('entity_display.repository')->getFormDisplay('user', 'user', 'register');
$user_registration_form->save();
// Check that the field does not appear on the registration form.
$this->drupalGet('user/register');
$this->webAssert->pageTextNotContains($field->label());
// Have the field appear on the registration form.
$user_registration_form->setComponent('test_user_field', ['type' => 'test_field_widget'])->save();
$this->drupalGet('user/register');
$this->webAssert->pageTextContains($field->label());
// In order to check the server side validation the native browser
// validation for required fields needs to be circumvented.
$session = $this->getSession();
$session->executeScript("jQuery('#edit-test-user-field-0-value').prop('required', false);");
// Check that validation errors are correctly reported.
$name = $this->randomMachineName();
$this->page->fillField('edit-name', $name);
$this->page->fillField('edit-mail', $name . '@example.com');
$this->page->pressButton('edit-submit');
$this->webAssert->pageTextContains($field->label() . ' field is required.');
// Invalid input.
$this->page->fillField('edit-test-user-field-0-value', '-1');
$this->page->pressButton('edit-submit');
$this->webAssert->pageTextContains($field->label() . ' does not accept the value -1.');
// Submit with valid data.
$value = (string) mt_rand(1, 255);
$this->page->fillField('edit-test-user-field-0-value', $value);
$this->page->pressButton('edit-submit');
// Check user fields.
$accounts = $this->container->get('entity_type.manager')->getStorage('user')
->loadByProperties(['name' => $name, 'mail' => $name . '@example.com']);
$new_user = reset($accounts);
$this->assertEquals($value, $new_user->test_user_field->value, 'The field value was correctly saved.');
// Check that the 'add more' button works.
$field_storage->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
$field_storage->save();
$name = $this->randomMachineName();
$this->drupalGet('user/register');
$this->page->fillField('edit-name', $name);
$this->page->fillField('edit-mail', $name . '@example.com');
$this->page->fillField('test_user_field[0][value]', $value);
// Add two inputs.
$this->page->pressButton('test_user_field_add_more');
$this->webAssert->waitForElement('css', 'input[name="test_user_field[1][value]"]');
$this->page->fillField('test_user_field[1][value]', $value . '1');
$this->page->pressButton('test_user_field_add_more');
$this->webAssert->waitForElement('css', 'input[name="test_user_field[2][value]"]');
$this->page->fillField('test_user_field[2][value]', $value . '2');
// Submit with three values.
$this->page->pressButton('edit-submit');
// Check user fields.
$accounts = $this->container->get('entity_type.manager')
->getStorage('user')
->loadByProperties(['name' => $name, 'mail' => $name . '@example.com']);
$new_user = reset($accounts);
$this->assertEquals($value, $new_user->test_user_field[0]->value);
$this->assertEquals($value . '1', $new_user->test_user_field[1]->value);
$this->assertEquals($value . '2', $new_user->test_user_field[2]->value);
}
}

View File

@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\FunctionalJavascript;
use Drupal\Core\Database\Database;
use Drupal\Core\Test\AssertMailTrait;
use Drupal\Core\Url;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\TestFileCreationTrait;
use Drupal\user\Entity\User;
/**
* Ensure that password reset methods work as expected.
*
* @group user
*/
class UserPasswordResetTest extends WebDriverTestBase {
use AssertMailTrait {
getMails as drupalGetMails;
}
use TestFileCreationTrait {
getTestFiles as drupalGetTestFiles;
}
/**
* The user object to test password resetting.
*
* @var \Drupal\user\UserInterface
*/
protected $account;
/**
* {@inheritdoc}
*/
protected static $modules = ['block', 'test_user_config'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a user.
$account = $this->drupalCreateUser(['access content']);
// Activate user by logging in.
$this->drupalLogin($account);
$this->account = User::load($account->id());
$this->account->pass_raw = $account->pass_raw;
$this->drupalLogout();
// Set the last login time that is used to generate the one-time link so
// that it is definitely over a second ago.
$account->login = \Drupal::time()->getRequestTime() - mt_rand(10, 100000);
Database::getConnection()->update('users_field_data')
->fields(['login' => $account->getLastLoginTime()])
->condition('uid', $account->id())
->execute();
}
/**
* Tests password reset functionality with an AJAX form.
*
* Make sure the ajax request from uploading a user picture does not
* invalidate the reset token.
*/
public function testUserPasswordResetWithAdditionalAjaxForm(): void {
$this->drupalGet(Url::fromRoute('user.reset.form', ['uid' => $this->account->id()]));
// Try to reset the password for an invalid account.
$this->drupalGet('user/password');
// Reset the password by username via the password reset page.
$edit['name'] = $this->account->getAccountName();
$this->submitForm($edit, 'Submit');
$resetURL = $this->getResetURL();
$this->drupalGet($resetURL);
// Login
$this->submitForm([], 'Log in');
// Generate file.
$image_file = current($this->drupalGetTestFiles('image'));
$image_path = \Drupal::service('file_system')->realpath($image_file->uri);
// Upload file.
$this->getSession()->getPage()->attachFileToField('Picture', $image_path);
$this->assertSession()->waitForButton('Remove');
// Change the forgotten password.
$password = \Drupal::service('password_generator')->generate();
$edit = ['pass[pass1]' => $password, 'pass[pass2]' => $password];
$this->submitForm($edit, 'Save');
// Verify that the password reset session has been destroyed.
$this->submitForm($edit, 'Save');
// Password needed to make profile changes.
$this->assertSession()->pageTextContains("Your current password is missing or incorrect; it's required to change the Password.");
}
/**
* Retrieves password reset email and extracts the login link.
*/
public function getResetURL() {
// Assume the most recent email.
$_emails = $this->drupalGetMails();
$email = end($_emails);
$urls = [];
preg_match('#.+user/reset/.+#', $email['body'], $urls);
return $urls[0];
}
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\user\RoleInterface;
/**
* Tests the JS components added to the user permissions page.
*
* @group user
*/
class UserPermissionsTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* User with admin privileges.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* User's role ID.
*
* @var string
*/
protected $rid;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->adminUser = $this->drupalCreateUser([
'administer permissions',
]);
// Find the new role ID.
$all_rids = $this->adminUser->getRoles();
unset($all_rids[array_search(RoleInterface::AUTHENTICATED_ID, $all_rids)]);
$this->rid = reset($all_rids);
}
/**
* Tests the dummy checkboxes added to the permissions page.
*/
public function testPermissionCheckboxes(): void {
$this->drupalLogin($this->adminUser);
$this->drupalGet('admin/people/permissions');
$page = $this->getSession()->getPage();
$wrapper = $page->find('css', '.form-item-' . $this->rid . '-administer-modules');
$real_checkbox = $wrapper->find('css', '.real-checkbox');
$dummy_checkbox = $wrapper->find('css', '.dummy-checkbox');
// The real per-role checkbox is visible and unchecked, the dummy copy is
// invisible.
$this->assertTrue($real_checkbox->isVisible());
$this->assertFalse($real_checkbox->isChecked());
$this->assertFalse($dummy_checkbox->isVisible());
// Enable the permission for all authenticated users.
$page->findField('authenticated[administer modules]')->click();
// The real and dummy checkboxes switch visibility and the dummy is now both
// checked and disabled.
$this->assertFalse($real_checkbox->isVisible());
$this->assertTrue($dummy_checkbox->isVisible());
$this->assertTrue($dummy_checkbox->isChecked());
$this->assertTrue($dummy_checkbox->hasAttribute('disabled'));
}
}

View File

@@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Condition;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\KernelTests\KernelTestBase;
use Drupal\user\Entity\Role;
use Drupal\user\Entity\User;
use Drupal\user\RoleInterface;
/**
* Tests the user role condition.
*
* @group user
*/
class UserRoleConditionTest extends KernelTestBase {
/**
* The condition plugin manager.
*
* @var \Drupal\Core\Condition\ConditionManager
*/
protected $manager;
/**
* An anonymous user for testing purposes.
*
* @var \Drupal\user\UserInterface
*/
protected $anonymous;
/**
* An authenticated user for testing purposes.
*
* @var \Drupal\user\UserInterface
*/
protected $authenticated;
/**
* A custom role for testing purposes.
*
* @var \Drupal\user\RoleInterface
*/
protected $role;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['system', 'user', 'field'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->manager = $this->container->get('plugin.manager.condition');
// Set up the authenticated and anonymous roles.
Role::create([
'id' => RoleInterface::ANONYMOUS_ID,
'label' => 'Anonymous user',
])->save();
Role::create([
'id' => RoleInterface::AUTHENTICATED_ID,
'label' => 'Authenticated user',
])->save();
// Create new role.
$rid = $this->randomMachineName(8);
$label = $this->randomString(8);
$role = Role::create([
'id' => $rid,
'label' => $label,
]);
$role->save();
$this->role = $role;
// Setup an anonymous user for our tests.
$this->anonymous = User::create([
'name' => '',
'uid' => 0,
]);
$this->anonymous->save();
// Loading the anonymous user adds the correct role.
$this->anonymous = User::load($this->anonymous->id());
// Setup an authenticated user for our tests.
$this->authenticated = User::create([
'name' => $this->randomMachineName(),
]);
$this->authenticated->save();
// Add the custom role.
$this->authenticated->addRole($this->role->id());
}
/**
* Tests the user_role condition.
*/
public function testConditions(): void {
// Grab the user role condition and configure it to check against
// authenticated user roles.
/** @var \Drupal\Core\Condition\ConditionInterface $condition */
$condition = $this->manager->createInstance('user_role')
->setConfig('roles', [RoleInterface::AUTHENTICATED_ID => RoleInterface::AUTHENTICATED_ID])
->setContextValue('user', $this->anonymous);
$this->assertFalse($condition->execute(), 'Anonymous users fail role checks for authenticated.');
// Check for the proper summary.
// Summaries require an extra space due to negate handling in summary().
$this->assertEquals('The user is a member of Authenticated user', $condition->summary());
// Set the user role to anonymous.
$condition->setConfig('roles', [RoleInterface::ANONYMOUS_ID => RoleInterface::ANONYMOUS_ID]);
$this->assertTrue($condition->execute(), 'Anonymous users pass role checks for anonymous.');
// Check for the proper summary.
$this->assertEquals('The user is a member of Anonymous user', $condition->summary());
// Set the user role to check anonymous or authenticated.
$condition->setConfig('roles', [RoleInterface::ANONYMOUS_ID => RoleInterface::ANONYMOUS_ID, RoleInterface::AUTHENTICATED_ID => RoleInterface::AUTHENTICATED_ID]);
$this->assertTrue($condition->execute(), 'Anonymous users pass role checks for anonymous or authenticated.');
// Check for the proper summary.
$this->assertEquals('The user is a member of Anonymous user, Authenticated user', $condition->summary());
// Set the context to the authenticated user and check that they also pass
// against anonymous or authenticated roles.
$condition->setContextValue('user', $this->authenticated);
$this->assertTrue($condition->execute(), 'Authenticated users pass role checks for anonymous or authenticated.');
// Set the role to just authenticated and recheck.
$condition->setConfig('roles', [RoleInterface::AUTHENTICATED_ID => RoleInterface::AUTHENTICATED_ID]);
$this->assertTrue($condition->execute(), 'Authenticated users pass role checks for authenticated.');
// Check the negated summary.
$condition->setConfig('negate', TRUE);
$this->assertEquals('The user is not a member of Authenticated user', $condition->summary());
// Check the complex negated summary.
$condition->setConfig('roles', [RoleInterface::ANONYMOUS_ID => RoleInterface::ANONYMOUS_ID, RoleInterface::AUTHENTICATED_ID => RoleInterface::AUTHENTICATED_ID]);
$this->assertEquals('The user is not a member of Anonymous user, Authenticated user', $condition->summary());
// Check a custom role.
$condition->setConfig('roles', [$this->role->id() => $this->role->id()]);
$condition->setConfig('negate', FALSE);
$this->assertTrue($condition->execute(), 'Authenticated user is a member of the custom role.');
$this->assertEquals(new FormattableMarkup('The user is a member of @roles', ['@roles' => $this->role->label()]), $condition->summary());
}
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\ContextProvider;
use Drupal\Core\Session\AccountInterface;
use Drupal\KernelTests\KernelTestBase;
use Drupal\user\Entity\User;
/**
* @coversDefaultClass \Drupal\user\ContextProvider\CurrentUserContext
*
* @group user
*/
class CurrentUserContextTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system', 'user'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
}
/**
* @covers ::getAvailableContexts
*/
public function testGetAvailableContexts(): void {
$context_repository = $this->container->get('context.repository');
// Test an authenticated account.
$authenticated = User::create([
'name' => $this->randomMachineName(),
]);
$authenticated->save();
$authenticated = User::load($authenticated->id());
$this->container->get('current_user')->setAccount($authenticated);
$contexts = $context_repository->getAvailableContexts();
$this->assertArrayHasKey('@user.current_user_context:current_user', $contexts);
$this->assertSame('entity:user', $contexts['@user.current_user_context:current_user']->getContextDefinition()->getDataType());
$this->assertTrue($contexts['@user.current_user_context:current_user']->hasContextValue());
$this->assertNotNull($contexts['@user.current_user_context:current_user']->getContextValue());
// Test an anonymous account.
$anonymous = $this->prophesize(AccountInterface::class);
$anonymous->id()->willReturn(0);
$this->container->get('current_user')->setAccount($anonymous->reveal());
$contexts = $context_repository->getAvailableContexts();
$this->assertArrayHasKey('@user.current_user_context:current_user', $contexts);
$this->assertSame('entity:user', $contexts['@user.current_user_context:current_user']->getContextDefinition()->getDataType());
$this->assertFalse($contexts['@user.current_user_context:current_user']->hasContextValue());
}
}

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Controller;
use Drupal\Core\Url;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\user\Controller\UserController;
/**
* Tests for the User controller.
*
* @group user
*
* @coversDefaultClass \Drupal\user\Controller\UserController
*/
class UserControllerTest extends KernelTestBase {
use UserCreationTrait;
/**
* The user controller.
*
* @var \Drupal\user\Controller\UserController
*/
protected $userController;
/**
* The logged in user.
*
* @var \Drupal\user\UserInterface
*/
protected $user;
/**
* {@inheritdoc}
*/
protected static $modules = [
'user',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->userController = UserController::create(\Drupal::getContainer());
// Create and log in a user.
$this->user = $this->setUpCurrentUser();
}
/**
* Tests the redirection to a user edit page.
*
* @covers ::userEditPage
*/
public function testUserEditPage(): void {
$response = $this->userController->userEditPage();
// Ensure the response is directed to the correct user edit page.
$edit_url = Url::fromRoute('entity.user.edit_form', [
'user' => $this->user->id(),
])->setAbsolute()
->toString();
$this->assertEquals($edit_url, $response->getTargetUrl());
$this->assertEquals(302, $response->getStatusCode());
}
}

View File

@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Field;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\KernelTests\KernelTestBase;
use Drupal\user\Entity\User;
/**
* Tests the user_name formatter.
*
* @group field
*/
class UserNameFormatterTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['field', 'user', 'system'];
/**
* @var string
*/
protected $entityType;
/**
* @var string
*/
protected $bundle;
/**
* @var string
*/
protected $fieldName;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(['field']);
$this->installEntitySchema('user');
$this->entityType = 'user';
$this->bundle = $this->entityType;
$this->fieldName = 'name';
}
/**
* Renders fields of a given entity with a given display.
*
* @param \Drupal\Core\Entity\FieldableEntityInterface $entity
* The entity object with attached fields to render.
* @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display
* The display to render the fields in.
*
* @return string
* The rendered entity fields.
*/
protected function renderEntityFields(FieldableEntityInterface $entity, EntityViewDisplayInterface $display) {
$content = $display->build($entity);
$content = $this->render($content);
return $content;
}
/**
* Tests the formatter output.
*/
public function testFormatter(): void {
$user = User::create([
'name' => 'test name',
]);
$user->save();
$result = $user->{$this->fieldName}->view(['type' => 'user_name']);
$this->assertEquals('username', $result[0]['#theme']);
$this->assertEquals(spl_object_hash($user), spl_object_hash($result[0]['#account']));
$result = $user->{$this->fieldName}->view(['type' => 'user_name', 'settings' => ['link_to_entity' => FALSE]]);
$this->assertEquals($user->getDisplayName(), $result[0]['#markup']);
$user = User::getAnonymousUser();
$result = $user->{$this->fieldName}->view(['type' => 'user_name']);
$this->assertEquals('username', $result[0]['#theme']);
$this->assertEquals(spl_object_hash($user), spl_object_hash($result[0]['#account']));
$result = $user->{$this->fieldName}->view(['type' => 'user_name', 'settings' => ['link_to_entity' => FALSE]]);
$this->assertEquals($user->getDisplayName(), $result[0]['#markup']);
$this->assertEquals($this->config('user.settings')->get('anonymous'), $result[0]['#markup']);
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
/**
* Tests deprecated user module functions.
*
* @group user
* @group legacy
*/
class LegacyUserTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'user',
];
/**
* Tests deprecation of user_role_permissions().
*/
public function testUserRolePermissions(): void {
$this->expectDeprecation('user_role_permissions() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no replacement beyond loading the roles and calling \Drupal\user\Entity\Role::getPermissions(). See https://www.drupal.org/node/3348138');
$expected = [
RoleInterface::ANONYMOUS_ID => [],
RoleInterface::AUTHENTICATED_ID => [],
];
$permissions = user_role_permissions(array_keys($expected));
$this->assertSame($expected, $permissions);
$permission = 'administer permissions';
$role = Role::create([
'id' => 'admin',
'label' => 'Test',
'is_admin' => TRUE,
'permissions' => [$permission],
]);
$role->save();
$permissions = user_role_permissions([$role->id()]);
$this->assertSame([$role->id() => []], $permissions);
$role
->setIsAdmin(FALSE)
->grantPermission($permission)
->save();
$permissions = user_role_permissions([$role->id()]);
$this->assertSame([$role->id() => [$permission]], $permissions);
}
}

View File

@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Migrate;
use Drupal\Tests\migrate\Kernel\MigrateTestBase;
use Drupal\user\Entity\User;
/**
* Tests preservation of root account password.
*
* @group user
*/
class MigrateUserAdminPassTest extends MigrateTestBase {
/**
* The passwords as retrieved from the account entities before migration.
*
* @var array
*/
protected $originalPasswords = [];
/**
* Modules to enable.
*
* @var string[]
*/
protected static $modules = ['user'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Make sure the admin user and a regular user are created.
$this->container->get('module_handler')->loadInclude('user', 'install');
$this->installEntitySchema('user');
user_install();
/** @var \Drupal\user\Entity\User $admin_account */
$admin_account = User::load(1);
$admin_account->setPassword('original');
$admin_account->save();
$this->originalPasswords[1] = $admin_account->getPassword();
/** @var \Drupal\user\Entity\User $user_account */
$user_account = User::create([
'uid' => 2,
'name' => 'original_username',
'mail' => 'original_email@example.com',
'pass' => 'original_password',
]);
$user_account->save();
$this->originalPasswords[2] = $user_account->getPassword();
}
/**
* Tests preserving the admin user's password.
*/
public function testAdminPasswordPreserved(): void {
$user_data_rows = [
[
'id' => '1',
'username' => 'site_admin',
'password' => 'new_password',
'email' => 'site_admin@example.com',
],
[
'id' => '2',
'username' => 'random_user',
'password' => 'random_password',
'email' => 'random_user@example.com',
],
];
$ids = ['id' => ['type' => 'integer']];
$definition = [
'id' => 'users',
'migration_tags' => ['Admin password test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => $user_data_rows,
'ids' => $ids,
],
'process' => [
'uid' => 'id',
'name' => 'username',
'mail' => 'email',
'pass' => 'password',
],
'destination' => ['plugin' => 'entity:user'],
];
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
$this->executeMigration($migration);
// Verify that admin username and email were changed, but password was not.
/** @var \Drupal\user\Entity\User $admin_account */
$admin_account = User::load(1);
$this->assertSame('site_admin', $admin_account->getAccountName());
$this->assertSame('site_admin@example.com', $admin_account->getEmail());
$this->assertSame($this->originalPasswords[1], $admin_account->getPassword());
// Verify that everything changed for the regular user.
/** @var \Drupal\user\Entity\User $user_account */
$user_account = User::load(2);
$this->assertSame('random_user', $user_account->getAccountName());
$this->assertSame('random_user@example.com', $user_account->getEmail());
$this->assertNotSame($this->originalPasswords[2], $user_account->getPassword());
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Migrate;
use Drupal\Tests\migrate_drupal\Kernel\MigrateDrupalTestBase;
use Drupal\migrate_drupal\Tests\StubTestTrait;
/**
* Test stub creation for user entities.
*
* @group user
*/
class MigrateUserStubTest extends MigrateDrupalTestBase {
use StubTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['user'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
}
/**
* Tests creation of user stubs.
*/
public function testStub(): void {
$this->performStubTest('user');
}
}

View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Migrate\d6;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
/**
* Tests field option translations migration.
*
* @group migrate_drupal_6
*/
class MigrateProfileFieldOptionTranslationTest extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'config_translation',
'language',
'locale',
'menu_ui',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->executeMigrations([
'language',
'user_profile_field',
'user_profile_field_instance',
'd6_profile_field_option_translation',
]);
}
/**
* Tests the Drupal 6 field option translation.
*/
public function testFieldOptionTranslation(): void {
$language_manager = $this->container->get('language_manager');
/** @var \Drupal\language\Config\LanguageConfigOverride $config_translation */
$config_translation = $language_manager->getLanguageConfigOverride('fr', 'field.storage.user.profile_count_trees');
$allowed_values = [
0 => [
'label' => 'fr - 10',
],
1 => [
'label' => 'fr - 20',
],
2 => [
'label' => 'fr - 50',
],
3 => [
'label' => 'fr - 100',
],
4 => [
'label' => 'fr - 1000',
],
];
$this->assertSame($allowed_values, $config_translation->get('settings.allowed_values'));
$config_translation = $language_manager->getLanguageConfigOverride('fr', 'field.storage.user.profile_sold_to');
$allowed_values = [
[
'label' => 'fr - Pill spammers Fitness spammers Back\slash Forward/slash Dot.in.the.middle',
],
];
$this->assertSame($allowed_values, $config_translation->get('settings.allowed_values'));
$config_translation = $language_manager->getLanguageConfigOverride('zu', 'field.storage.user.profile_sold_to');
$allowed_values = [
[
'label' => 'zu - Pill spammers Fitness spammers Back\slash Forward/slash Dot.in.the.middle',
],
];
$this->assertSame($allowed_values, $config_translation->get('settings.allowed_values'));
}
}

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Migrate\d6;
use Drupal\Tests\SchemaCheckTestTrait;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
use Drupal\user\AccountSettingsForm;
use Drupal\Core\Database\Database;
use Drupal\user\UserInterface;
/**
* Upgrade variables to user.*.yml.
*
* @group migrate_drupal_6
*/
class MigrateUserConfigsTest extends MigrateDrupal6TestBase {
use SchemaCheckTestTrait;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->executeMigrations(['d6_user_mail', 'd6_user_settings']);
}
/**
* Tests migration of user variables to user.mail.yml.
*/
public function testUserMail(): void {
$config = $this->config('user.mail');
$this->assertSame('Account details for [user:name] at [site:name] (approved)', $config->get('status_activated.subject'));
$this->assertSame("[user:name],\n\nYour account at [site:name] has been activated.\n\nYou may now log in by clicking on this link or copying and pasting it in your browser:\n\n[user:one-time-login-url]\n\nThis is a one-time login, so it can be used only once.\n\nAfter logging in, you will be redirected to [user:edit-url] so you can change your password.\n\nOnce you have set your own password, you will be able to log in to [site:login-url] in the future using:\n\nusername: [user:name]\n", $config->get('status_activated.body'));
$this->assertSame('Replacement login information for [user:name] at [site:name]', $config->get('password_reset.subject'));
$this->assertSame("[user:name],\n\nA request to reset the password for your account has been made at [site:name].\n\nYou may now log in to [site:url-brief] by clicking on this link or copying and pasting it in your browser:\n\n[user:one-time-login-url]\n\nThis is a one-time login, so it can be used only once. It expires after one day and nothing will happen if it's not used.\n\nAfter logging in, you will be redirected to [user:edit-url] so you can change your password.", $config->get('password_reset.body'));
$this->assertSame('Account details for [user:name] at [site:name] (deleted)', $config->get('cancel_confirm.subject'));
$this->assertSame("[user:name],\n\nYour account on [site:name] has been deleted.", $config->get('cancel_confirm.body'));
$this->assertSame('An administrator created an account for you at [site:name]', $config->get('register_admin_created.subject'));
$this->assertSame("[user:name],\n\nA site administrator at [site:name] has created an account for you. You may now log in to [site:login-url] using the following username and password:\n\nusername: [user:name]\npassword: \n\nYou may also log in by clicking on this link or copying and pasting it in your browser:\n\n[user:one-time-login-url]\n\nThis is a one-time login, so it can be used only once.\n\nAfter logging in, you will be redirected to [user:edit-url] so you can change your password.\n\n\n-- [site:name] team", $config->get('register_admin_created.body'));
$this->assertSame('Account details for [user:name] at [site:name]', $config->get('register_no_approval_required.subject'));
$this->assertSame("[user:name],\n\nThank you for registering at [site:name]. You may now log in to [site:login-url] using the following username and password:\n\nusername: [user:name]\npassword: \n\nYou may also log in by clicking on this link or copying and pasting it in your browser:\n\n[user:one-time-login-url]\n\nThis is a one-time login, so it can be used only once.\n\nAfter logging in, you will be redirected to [user:edit-url] so you can change your password.\n\n\n-- [site:name] team", $config->get('register_no_approval_required.body'));
$this->assertSame('Account details for [user:name] at [site:name] (pending admin approval)', $config->get('register_pending_approval.subject'));
$this->assertSame("[user:name],\n\nThank you for registering at [site:name]. Your application for an account is currently pending approval. Once it has been approved, you will receive another email containing information about how to log in, set your password, and other details.\n\n\n-- [site:name] team", $config->get('register_pending_approval.body'));
$this->assertSame('Account details for [user:name] at [site:name] (blocked)', $config->get('status_blocked.subject'));
$this->assertSame("[user:name],\n\nYour account on [site:name] has been blocked.", $config->get('status_blocked.body'));
$this->assertConfigSchema(\Drupal::service('config.typed'), 'user.mail', $config->get());
// Tests migration of user variables to user.settings.yml.
$config = $this->config('user.settings');
$this->assertTrue($config->get('notify.status_blocked'));
$this->assertFalse($config->get('notify.status_activated'));
$this->assertFalse($config->get('verify_mail'));
$this->assertSame('admin_only', $config->get('register'));
$this->assertSame('Guest', $config->get('anonymous'));
// Tests migration of user_register using the AccountSettingsForm.
// Map D6 value to D8 value
$user_register_map = [
[0, UserInterface::REGISTER_ADMINISTRATORS_ONLY],
[1, UserInterface::REGISTER_VISITORS],
[2, UserInterface::REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL],
];
foreach ($user_register_map as $map) {
// Tests migration of user_register = 1
Database::getConnection('default', 'migrate')
->update('variable')
->fields(['value' => serialize($map[0])])
->condition('name', 'user_register')
->execute();
/** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
$migration = $this->getMigration('d6_user_settings');
// Indicate we're rerunning a migration that's already run.
$migration->getIdMap()->prepareUpdate();
$this->executeMigration($migration);
$form = $this->container->get('form_builder')->getForm(AccountSettingsForm::create($this->container));
$this->assertSame($map[1], $form['registration_cancellation']['user_register']['#value']);
}
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Migrate\d6;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
/**
* Users contact settings migration.
*
* @group migrate_drupal_6
*/
class MigrateUserContactSettingsTest extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['contact'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->migrateUsers(FALSE);
$this->installSchema('user', ['users_data']);
$this->executeMigration('d6_user_contact_settings');
}
/**
* Tests the Drupal6 user contact settings migration.
*/
public function testUserContactSettings(): void {
$user_data = \Drupal::service('user.data');
$module = $key = 'contact';
$uid = 2;
$setting = $user_data->get($module, $uid, $key);
$this->assertSame('1', $setting);
$uid = 8;
$setting = $user_data->get($module, $uid, $key);
$this->assertSame('0', $setting);
$uid = 15;
$setting = $user_data->get($module, $uid, $key);
$this->assertNull($setting);
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Migrate\d6;
use Drupal\file\Entity\File;
use Drupal\file\FileInterface;
use Drupal\Tests\file\Kernel\Migrate\d6\FileMigrationTestTrait;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
/**
* User pictures migration.
*
* @group migrate_drupal_6
*/
class MigrateUserPictureD6FileTest extends MigrateDrupal6TestBase {
use FileMigrationTestTrait;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('file');
$this->executeMigration('d6_user_picture_file');
}
/**
* Tests the Drupal 6 user pictures to Drupal 8 migration.
*/
public function testUserPictures(): void {
$file_ids = [];
foreach ($this->migration->getIdMap() as $destination_ids) {
$file_ids[] = reset($destination_ids);
}
$files = File::loadMultiple($file_ids);
/** @var \Drupal\file\FileInterface $file */
$file = array_shift($files);
$this->assertSame('image-test.jpg', $file->getFilename());
$this->assertSame('public://image-test.jpg', $file->getFileUri());
$this->assertSame('2', $file->getOwnerId());
$this->assertSame(1901, $file->getSize());
$this->assertSame('image/jpeg', $file->getMimeType());
$file = array_shift($files);
$this->assertSame('image-test.png', $file->getFilename());
$this->assertSame('public://image-test.png', $file->getFileUri());
$this->assertSame('8', $file->getOwnerId());
$this->assertEmpty($files);
// Tests the D6 user pictures migration in combination with D6 file.
$this->setUpMigratedFiles();
$this->assertEntity(1, 'image-test.jpg', 1901, 'public://image-test.jpg', 'image/jpeg', 2);
$this->assertEntity(2, 'image-test.png', 125, 'public://image-test.png', 'image/png', 8);
$this->assertEntity(3, 'Image1.png', 39325, 'public://image-1.png', 'image/png', 1);
$this->assertEntity(4, 'Image2.jpg', 1831, 'public://image-2.jpg', 'image/jpeg', 1);
$this->assertEntity(5, 'Image-test.gif', 183, 'public://image-test.gif', 'image/jpeg', 1);
$this->assertEntity(6, 'html-1.txt', 19, 'public://html-1.txt', 'text/plain', 1);
}
/**
* Asserts a file entity.
*
* @param int $fid
* The file ID.
* @param string $name
* The expected file name.
* @param int $size
* The expected file size.
* @param string $uri
* The expected file URI.
* @param string $type
* The expected MIME type.
* @param int $uid
* The expected file owner ID.
*
* @internal
*/
protected function assertEntity(int $fid, string $name, int $size, string $uri, string $type, int $uid): void {
/** @var \Drupal\file\FileInterface $file */
$file = File::load($fid);
$this->assertInstanceOf(FileInterface::class, $file);
$this->assertSame($name, $file->getFilename());
$this->assertSame($size, (int) $file->getSize());
$this->assertSame($uri, $file->getFileUri());
$this->assertSame($type, $file->getMimeType());
$this->assertSame($uid, (int) $file->getOwnerId());
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Migrate\d6;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
/**
* Tests the user profile entity display migration.
*
* @group migrate_drupal_6
*/
class MigrateUserProfileEntityDisplayTest extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->executeMigrations([
'user_profile_field',
'user_profile_field_instance',
'user_profile_entity_display',
]);
}
/**
* Tests migration of user profile fields.
*/
public function testUserProfileFields(): void {
$display = EntityViewDisplay::load('user.user.default');
// Test a text field.
$component = $display->getComponent('profile_color');
$this->assertSame('text_default', $component['type']);
// Test a list field.
$component = $display->getComponent('profile_bands');
$this->assertSame('text_default', $component['type']);
// Test a date field.
$component = $display->getComponent('profile_birthdate');
$this->assertSame('datetime_default', $component['type']);
// Test PROFILE_PRIVATE field is hidden.
$this->assertNull($display->getComponent('profile_sell_address'));
// Test PROFILE_HIDDEN field is hidden.
$this->assertNull($display->getComponent('profile_sold_to'));
// Test a checkbox field.
$component = $display->getComponent('profile_really_really_love_mig');
$this->assertSame('list_default', $component['type']);
}
}

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Migrate\d6;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
/**
* Tests the user profile entity form display migration.
*
* @group migrate_drupal_6
*/
class MigrateUserProfileEntityFormDisplayTest extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->executeMigrations([
'user_profile_field',
'user_profile_field_instance',
'user_profile_entity_form_display',
]);
}
/**
* Tests migration of user profile fields.
*/
public function testUserProfileEntityFormDisplay(): void {
$display = EntityFormDisplay::load('user.user.default');
// Test a text field.
$component = $display->getComponent('profile_color');
$this->assertSame('text_textfield', $component['type']);
// Test a list field.
$component = $display->getComponent('profile_bands');
$this->assertSame('text_textfield', $component['type']);
// Test a date field.
$component = $display->getComponent('profile_birthdate');
$this->assertSame('datetime_default', $component['type']);
// Test PROFILE_PRIVATE field is hidden.
$this->assertNull($display->getComponent('profile_sell_address'));
// Test PROFILE_HIDDEN field is hidden.
$this->assertNull($display->getComponent('profile_sold_to'));
// Test that a checkbox field has the proper display label setting.
$component = $display->getComponent('profile_really_really_love_mig');
$this->assertSame('boolean_checkbox', $component['type']);
$this->assertTrue($component['settings']['display_label']);
}
}

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Migrate\d6;
use Drupal\field\Entity\FieldConfig;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
/**
* Tests the user profile field instance migration.
*
* @group migrate_drupal_6
*/
class MigrateUserProfileFieldInstanceTest extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['field'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->executeMigrations([
'user_profile_field',
'user_profile_field_instance',
]);
}
/**
* Tests migration of user profile fields.
*/
public function testUserProfileFields(): void {
// Migrated a text field.
$field = FieldConfig::load('user.user.profile_color');
$this->assertSame('Favorite color', $field->label());
$this->assertSame('List your favorite color', $field->getDescription());
// Migrated a textarea.
$field = FieldConfig::load('user.user.profile_biography');
$this->assertSame('Biography', $field->label());
$this->assertSame('Tell people a little bit about yourself', $field->getDescription());
// Migrated checkbox field.
$field = FieldConfig::load('user.user.profile_sell_address');
$this->assertSame('Sell your email address?', $field->label());
$this->assertSame("If you check this box, we'll sell your address to spammers to help line the pockets of our shareholders. Thanks!", $field->getDescription());
// Migrated selection field.
$field = FieldConfig::load('user.user.profile_sold_to');
$this->assertSame('Sales Category', $field->label());
$this->assertSame("Select the sales categories to which this user's address was sold.", $field->getDescription());
// Migrated list field.
$field = FieldConfig::load('user.user.profile_bands');
$this->assertSame('Favorite bands', $field->label());
$this->assertSame("Enter your favorite bands. When you've saved your profile, you'll be able to find other people with the same favorites.", $field->getDescription());
// Migrated URL field.
$field = FieldConfig::load('user.user.profile_blog');
$this->assertSame('Blog', $field->label());
$this->assertSame("Paste the full URL, including http://, of your personal blog.", $field->getDescription());
// Migrated date field.
$field = FieldConfig::load('user.user.profile_birthdate');
$this->assertSame('Birthdate', $field->label());
$this->assertSame("Enter your birth date and we'll send you a coupon.", $field->getDescription());
// Another migrated checkbox field, with a different source visibility setting.
$field = FieldConfig::load('user.user.profile_really_really_love_mig');
$this->assertSame('I really, really, really love migrations', $field->label());
$this->assertSame("If you check this box, you love migrations.", $field->getDescription());
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Migrate\d6;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
/**
* Tests the user profile field migration.
*
* @group migrate_drupal_6
*/
class MigrateUserProfileFieldTest extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->executeMigration('user_profile_field');
}
/**
* Tests migration of user profile fields.
*/
public function testUserProfileFields(): void {
// Migrated a text field.
$field_storage = FieldStorageConfig::load('user.profile_color');
$this->assertSame('text', $field_storage->getType(), 'Field type is text.');
$this->assertSame(1, $field_storage->getCardinality(), 'Text field has correct cardinality');
// Migrated a textarea.
$field_storage = FieldStorageConfig::load('user.profile_biography');
$this->assertSame('text_long', $field_storage->getType(), 'Field type is text_long.');
// Migrated checkbox field.
$field_storage = FieldStorageConfig::load('user.profile_sell_address');
$this->assertSame('boolean', $field_storage->getType(), 'Field type is boolean.');
// Migrated selection field.
$field_storage = FieldStorageConfig::load('user.profile_sold_to');
$this->assertSame('list_string', $field_storage->getType(), 'Field type is list_string.');
$settings = $field_storage->getSettings();
$this->assertEquals(['Pill spammers' => 'Pill spammers', 'Fitness spammers' => 'Fitness spammers', 'Back\\slash' => 'Back\\slash', 'Forward/slash' => 'Forward/slash', 'Dot.in.the.middle' => 'Dot.in.the.middle', 'Faithful servant' => 'Faithful servant', 'Anonymous donor' => 'Anonymous donor'], $settings['allowed_values']);
$this->assertSame('list_string', $field_storage->getType(), 'Field type is list_string.');
// Migrated list field.
$field_storage = FieldStorageConfig::load('user.profile_bands');
$this->assertSame('text', $field_storage->getType(), 'Field type is text.');
$this->assertSame(-1, $field_storage->getCardinality(), 'List field has correct cardinality');
// Migrated URL field.
$field_storage = FieldStorageConfig::load('user.profile_blog');
$this->assertSame('link', $field_storage->getType(), 'Field type is link.');
// Migrated date field.
$field_storage = FieldStorageConfig::load('user.profile_birthdate');
$this->assertSame('datetime', $field_storage->getType(), 'Field type is datetime.');
$this->assertSame('date', $field_storage->getSettings()['datetime_type']);
}
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Migrate\d6;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
use Drupal\user\Entity\User;
/**
* User profile values migration.
*
* @group migrate_drupal_6
*/
class MigrateUserProfileValuesTest extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['language'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->executeMigrations([
'language',
'user_profile_field',
'user_profile_field_instance',
'user_profile_entity_display',
'user_profile_entity_form_display',
]);
$this->migrateUsers(FALSE);
$this->executeMigration('d6_profile_values');
}
/**
* Tests Drupal 6 profile values to Drupal 8 migration.
*/
public function testUserProfileValues(): void {
$user = User::load(2);
$this->assertNotNull($user);
$this->assertSame('red', $user->profile_color->value);
// cSpell:disable
$expected = <<<EOT
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam nulla sapien, congue nec risus ut, adipiscing aliquet felis. Maecenas quis justo vel nulla varius euismod. Quisque metus metus, cursus sit amet sem non, bibendum vehicula elit. Cras dui nisl, eleifend at iaculis vitae, lacinia ut felis. Nullam aliquam ligula volutpat nulla consectetur accumsan. Maecenas tincidunt molestie diam, a accumsan enim fringilla sit amet. Morbi a tincidunt tellus. Donec imperdiet scelerisque porta. Sed quis sem bibendum eros congue sodales. Vivamus vel fermentum est, at rutrum orci. Nunc consectetur purus ut dolor pulvinar, ut volutpat felis congue. Cras tincidunt odio sed neque sollicitudin, vehicula tempor metus scelerisque.
EOT;
// cSpell:enable
$this->assertSame($expected, $user->profile_biography->value);
$this->assertSame('1', $user->profile_sell_address->value);
$this->assertSame('Back\slash', $user->profile_sold_to->value);
$this->assertSame('AC/DC', $user->profile_bands[0]->value);
$this->assertSame('Eagles', $user->profile_bands[1]->value);
$this->assertSame('Elton John', $user->profile_bands[2]->value);
$this->assertSame('Lemonheads', $user->profile_bands[3]->value);
$this->assertSame('Rolling Stones', $user->profile_bands[4]->value);
$this->assertSame('Queen', $user->profile_bands[5]->value);
$this->assertSame('The White Stripes', $user->profile_bands[6]->value);
$this->assertSame('1974-06-02', $user->profile_birthdate->value);
$this->assertSame('http://example.com/blog', $user->profile_blog->uri);
$this->assertNull($user->profile_blog->title);
$this->assertSame([], $user->profile_blog->options);
$this->assertSame('http://example.com/blog', $user->profile_blog->uri);
// Check that the source profile field names that are longer than 32
// characters have been migrated.
$this->assertNotNull($user->getFieldDefinition('profile_really_really_love_mig'));
$this->assertNotNull($user->getFieldDefinition('profile_really_really_love_mig1'));
$this->assertSame('1', $user->profile_really_really_love_mig->value);
$this->assertNull($user->profile_really_really_love_mig1->value);
$user = User::load(8);
$this->assertSame('Forward/slash', $user->profile_sold_to->value);
$user = User::load(15);
$this->assertSame('Dot.in.the.middle', $user->profile_sold_to->value);
}
}

View File

@@ -0,0 +1,346 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Migrate\d6;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
/**
* Upgrade user roles to user.role.*.yml.
*
* @group migrate_drupal_6
*/
class MigrateUserRoleTest extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->startCollectingMessages();
}
/**
* Assert the logged migrate messages.
*
* @param string[][] $role_data
* An array of role data keyed by the destination role id. The role data
* contains the source role id, an array of valid permissions and an array
* of invalid permissions.
* @param \Drupal\migrate\Plugin\MigrateIdMapInterface $id_map
* The migration ID map plugin.
*/
public function assertMessages(array $role_data, MigrateIdMapInterface $id_map) {
foreach ($id_map->getMessages() as $message) {
$permissions = implode("', '", $role_data[$message->dest_id]['invalid']);
$expected_message = "Permission(s) '" . $permissions . "' not found.";
$this->assertSame($expected_message, $message->message);
$this->assertSame(MigrationInterface::MESSAGE_WARNING, (int) $message->level);
}
}
/**
* Asserts there are no duplicate roles.
*/
public function assertNoDuplicateRoles() {
$roles = [
'anonymous1',
'authenticated1',
'administrator1',
'migrate_test_role_11',
'migrate_test_role_21',
'migrate_test_role_3_that_is_longer_than_thirty_two_characters1',
'migrate_test_role_41',
];
$this->assertEmpty(Role::loadMultiple($roles));
}
/**
* Helper function to perform assertions on a user role.
*
* @param string $id
* The role ID.
* @param string[] $permissions
* An array of user permissions.
* @param int $lookupId
* The original numeric ID of the role in the source database.
* @param \Drupal\migrate\Plugin\MigrateIdMapInterface $id_map
* The map table plugin.
*
* @internal
*/
protected function assertRole(string $id, array $permissions, int $lookupId, MigrateIdMapInterface $id_map): void {
/** @var \Drupal\user\RoleInterface $role */
$role = Role::load($id);
$this->assertInstanceOf(RoleInterface::class, $role);
$this->assertSame($permissions, $role->getPermissions());
$this->assertSame([[$id]], $id_map->lookupDestinationIds(['rid' => $lookupId]));
}
/**
* Helper to assert the user roles.
*
* @param array $permissions
* Contains the valid and invalid permissions.
* @param \Drupal\migrate\Plugin\MigrateIdMapInterface $id_map
* The map table plugin.
*
* @internal
*/
protected function assertRoles(array $permissions, MigrateIdMapInterface $id_map): void {
foreach ($permissions as $rid => $datum) {
$this->assertRole($rid, $datum['valid'], $datum['rid'], $id_map);
}
}
/**
* Data provider for user role migration tests.
*/
public static function providerTestUserRole() {
return [
'filter only' => [
'modules' => [],
'migrations' => [
'd6_filter_format',
'd6_user_role',
],
'role_data' => [
'anonymous' => [
'rid' => 1,
'valid' => [
'access content',
'use text format filtered_html',
],
'invalid' => [
'migrate test anonymous permission',
],
],
'authenticated' => [
'rid' => 2,
'valid' => [
'access content',
'use text format filtered_html',
],
'invalid' => [
'access comments',
'migrate test authenticated permission',
'post comments',
'skip comment approval',
],
],
'migrate_test_role_1' => [
'rid' => 3,
'valid' => [
'use text format full_html',
'use text format php_code',
],
'invalid' => [
'migrate test role 1 test permission',
],
],
'migrate_test_role_2' => [
'rid' => 4,
'valid' => [
'access content overview',
'administer nodes',
'use text format php_code',
],
'invalid' => [
'administer contact forms',
'create forum content',
'delete any blog content',
'delete any forum content',
'delete own blog content',
'delete own forum content',
'edit any blog content',
'edit any forum content',
'edit own blog content',
'edit own forum content',
'migrate test role 2 test permission',
'skip comment approval',
'use PHP for settings',
],
],
'migrate_test_role_3_that_is_longer_than_thirty_two_characters' => [
'rid' => 5,
'valid' => [
'use text format php_code',
],
'invalid' => [],
],
],
],
'all dependent migrations' => [
'modules' => [
'block',
'block_content',
'comment',
'contact',
'config_translation',
'language',
'link',
'menu_ui',
'node',
'taxonomy',
'text',
],
'migrations' => [
'language',
'd6_comment_type',
'block_content_type',
'contact_category',
'd6_filter_format',
'd6_taxonomy_vocabulary',
'd6_taxonomy_vocabulary_translation',
'd6_user_role',
],
'role_data' => [
'anonymous' => [
'rid' => 1,
'valid' => [
'access content',
'use text format filtered_html',
],
'invalid' => [
'migrate test anonymous permission',
],
],
'authenticated' => [
'rid' => 2,
'valid' => [
'access comments',
'access content',
'post comments',
'skip comment approval',
'use text format filtered_html',
],
'invalid' => [
'migrate test authenticated permission',
],
],
'migrate_test_role_1' => [
'rid' => 3,
'valid' => [
'use text format full_html',
'use text format php_code',
],
'invalid' => [
'migrate test role 1 test permission',
],
],
'migrate_test_role_2' => [
'rid' => 4,
'valid' => [
'access content overview',
'administer contact forms',
'administer nodes',
'create forum content',
'delete any forum content',
'delete own forum content',
'edit any forum content',
'edit own forum content',
'skip comment approval',
'use text format php_code',
],
'invalid' => [
'delete any blog content',
'delete own blog content',
'edit any blog content',
'edit own blog content',
'migrate test role 2 test permission',
'use PHP for settings',
],
],
'migrate_test_role_3_that_is_longer_than_thirty_two_characters' => [
'rid' => 5,
'valid' => [
'use text format php_code',
],
'invalid' => [],
],
],
],
];
}
/**
* Tests user role migration.
*
* @param string[] $modules
* A list of modules to install.
* @param string[] $migrations
* A list of migrations to execute.
* @param string[][] $role_data
* An array of role data keyed by the destination role id. The role data
* contains the source role id, an array of valid permissions and an array
* of invalid permissions.
*
* @dataProvider providerTestUserRole
*/
public function testUserRole(array $modules, array $migrations, array $role_data): void {
if ($modules) {
// Install modules that have migrations that may provide permissions.
\Drupal::service('module_installer')->install($modules);
$this->installEntitySchema('block_content');
$this->installConfig(['block_content', 'comment']);
$this->migrateContentTypes();
}
$this->executeMigrations($migrations);
$id_map = $this->getMigration('d6_user_role')->getIdMap();
// After all the migrations are run, there are changes to the permissions.
$this->assertRoles($role_data, $id_map);
$roles = [
'anonymous1',
'authenticated1',
'administrator1',
'migrate_test_role_11',
'migrate_test_role_21',
'migrate_test_role_3_that_is_longer_than_thirty_two_characters1',
];
$this->assertEmpty(Role::loadMultiple($roles));
$this->assertMessages($role_data, $id_map);
$this->assertSame(4, $id_map->messageCount());
// Remove the map row for the migrate_test_role_1 role and rerun the
// migration. This will re-import the migrate_test_role_1 role migration
// again.
$this->sourceDatabase->insert('role')
->fields([
'rid' => 6,
'name' => 'migrate test role 4',
])
->execute();
$this->sourceDatabase->insert('permission')
->fields([
'pid' => 7,
'rid' => 6,
'perm' => 'access content',
'tid' => 0,
])
->execute();
$id_map->delete(['rid' => 3]);
$this->executeMigration('d6_user_role');
// Test there are no duplicated roles.
$this->assertNoDuplicateRoles();
// Test that the existing roles have not changed.
$this->assertRoles($role_data, $id_map);
// Test the migration of the new role, migrate_test_role_4.
$permissions = ['access content'];
$this->assertRole('migrate_test_role_4', $permissions, 6, $id_map);
}
}

View File

@@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Migrate\d6;
use Drupal\migrate\MigrateExecutable;
use Drupal\Tests\file\Kernel\Migrate\d6\FileMigrationTestTrait;
use Drupal\user\Entity\User;
use Drupal\file\Entity\File;
use Drupal\Core\Database\Database;
use Drupal\user\RoleInterface;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
/**
* Users migration.
*
* @group migrate_drupal_6
*/
class MigrateUserTest extends MigrateDrupal6TestBase {
use FileMigrationTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['language', 'phpass'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('file');
$this->installSchema('file', ['file_usage']);
$this->installEntitySchema('node');
$this->installSchema('user', ['users_data']);
// Make sure uid 1 is created.
user_install();
$file = File::create([
'fid' => 2,
'uid' => 2,
'filename' => 'image-test.jpg',
'uri' => "public://image-test.jpg",
'filemime' => 'image/jpeg',
'created' => 1,
'changed' => 1,
]);
$file->setPermanent();
$file->enforceIsNew();
file_put_contents($file->getFileUri(), file_get_contents('core/tests/fixtures/files/image-1.png'));
$file->save();
$file = File::create([
'fid' => 8,
'uid' => 8,
'filename' => 'image-test.png',
'uri' => "public://image-test.png",
'filemime' => 'image/png',
'created' => 1,
'changed' => 1,
]);
$file->setPermanent();
$file->enforceIsNew();
file_put_contents($file->getFileUri(), file_get_contents('core/tests/fixtures/files/image-2.jpg'));
$file->save();
$this->executeMigration('language');
$this->migrateUsers();
}
/**
* Tests the Drupal6 user to Drupal 8 migration.
*/
public function testUser(): void {
$users = Database::getConnection('default', 'migrate')
->select('users', 'u')
->fields('u')
->condition('uid', 0, '>')
->execute()
->fetchAll();
foreach ($users as $source) {
// Get roles directly from the source.
$rids = Database::getConnection('default', 'migrate')
->select('users_roles', 'ur')
->fields('ur', ['rid'])
->condition('ur.uid', $source->uid)
->execute()
->fetchCol();
$roles = [RoleInterface::AUTHENTICATED_ID];
$id_map = $this->getMigration('d6_user_role')->getIdMap();
foreach ($rids as $rid) {
$role = $id_map->lookupDestinationIds([$rid])[0];
$roles[] = reset($role);
}
/** @var \Drupal\user\UserInterface $user */
$user = User::load($source->uid);
$this->assertSame($source->uid, $user->id());
$this->assertSame($source->name, $user->label());
$this->assertSame($source->mail, $user->getEmail());
$this->assertSame($source->created, $user->getCreatedTime());
$this->assertSame($source->access, $user->getLastAccessedTime());
$this->assertSame($source->login, $user->getLastLoginTime());
$is_blocked = $source->status == 0;
$this->assertSame($is_blocked, $user->isBlocked());
$expected_timezone_name = $source->timezone_name ?: $this->config('system.date')->get('timezone.default');
$this->assertSame($expected_timezone_name, $user->getTimeZone());
$this->assertSame($source->init, $user->getInitialEmail());
$this->assertSame($roles, $user->getRoles());
// Ensure the user's langcode, preferred_langcode and
// preferred_admin_langcode are valid.
// $user->getPreferredLangcode() might fallback to default language if the
// user preferred language is not configured on the site. We just want to
// test if the value was imported correctly.
$language_manager = $this->container->get('language_manager');
$default_langcode = $language_manager->getDefaultLanguage()->getId();
if (empty($source->language)) {
$this->assertSame('en', $user->langcode->value);
$this->assertSame($default_langcode, $user->preferred_langcode->value);
$this->assertSame($default_langcode, $user->preferred_admin_langcode->value);
}
elseif ($language_manager->getLanguage($source->language) === NULL) {
$this->assertSame($default_langcode, $user->langcode->value);
$this->assertSame($default_langcode, $user->preferred_langcode->value);
$this->assertSame($default_langcode, $user->preferred_admin_langcode->value);
}
else {
$this->assertSame($source->language, $user->langcode->value);
$this->assertSame($source->language, $user->preferred_langcode->value);
$this->assertSame($source->language, $user->preferred_admin_langcode->value);
}
// We have one empty picture in the data so don't try load that.
if (!empty($source->picture)) {
// Test the user picture.
$file = File::load($user->user_picture->target_id);
$this->assertSame(basename($source->picture), $file->getFilename());
}
else {
// Ensure the user does not have a picture.
$this->assertEmpty($user->user_picture->target_id, sprintf('User %s does not have a picture', $user->id()));
}
// Use the API to check if the password has been salted and re-hashed to
// conform to Drupal >= 7 for non-admin users.
if ($user->id() != 1) {
$this->assertTrue(\Drupal::service('password')
->check($source->pass_plain, $user->getPassword()));
}
}
// Rollback the migration and make sure everything is deleted but uid 1.
(new MigrateExecutable($this->migration, $this))->rollback();
$users = Database::getConnection('default', 'migrate')
->select('users', 'u')
->fields('u', ['uid'])
->condition('uid', 0, '>')
->execute()
->fetchCol();
foreach ($users as $uid) {
$account = User::load($uid);
if ($uid == 1) {
$this->assertNotNull($account, 'User 1 was preserved after rollback');
}
else {
$this->assertNull($account);
}
}
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Migrate\d6;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
use Drupal\migrate\Exception\RequirementsException;
/**
* Tests check requirements for profile_field source plugin.
*
* @group user
*/
class ProfileFieldCheckRequirementsTest extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->sourceDatabase->schema()->dropTable('profile_fields');
}
/**
* Tests exception is thrown when profile_fields tables do not exist.
*/
public function testCheckRequirements(): void {
$this->expectException(RequirementsException::class);
$this->expectExceptionMessage('Profile module not enabled on source site');
$this->getMigration('user_profile_field')
->getSourcePlugin()
->checkRequirements();
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Migrate\d7;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
/**
* Migrates user flood control configuration.
*
* @group user
*/
class MigrateUserFloodTest extends MigrateDrupal7TestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(['user']);
$this->executeMigration('d7_user_flood');
}
/**
* Tests the migration.
*/
public function testMigration(): void {
$expected = [
'_core' => [
'default_config_hash' => 'UYfMzeP1S8jKm9PSvxf7nQNe8DsNS-3bc2WSNNXBQWs',
],
'uid_only' => TRUE,
'ip_limit' => 30,
'ip_window' => 7200,
'user_limit' => 22,
'user_window' => 86400,
];
$this->assertSame($expected, $this->config('user.flood')->get());
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Migrate\d7;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
/**
* Migrates user mail configuration.
*
* @group user
*/
class MigrateUserMailTest extends MigrateDrupal7TestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(['user']);
$this->executeMigration('d7_user_mail');
}
/**
* Tests the migration.
*/
public function testMigration(): void {
$config = $this->config('user.mail');
$this->assertSame('Your account is approved!', $config->get('status_activated.subject'));
$this->assertSame('Your account was activated, and there was much rejoicing.', $config->get('status_activated.body'));
$this->assertSame('Fix your password', $config->get('password_reset.subject'));
$this->assertSame("Nope! You're locked out forever.", $config->get('password_reset.body'));
$this->assertSame('So long, bub', $config->get('cancel_confirm.subject'));
$this->assertSame('The gates of Drupal are closed to you. Now you will work in the salt mines.', $config->get('cancel_confirm.body'));
$this->assertSame('Gawd made you an account', $config->get('register_admin_created.subject'));
$this->assertSame('...and it could be taken away.', $config->get('register_admin_created.body'));
$this->assertSame('Welcome!', $config->get('register_no_approval_required.subject'));
$this->assertSame('You can now log in if you can figure out how to use Drupal!', $config->get('register_no_approval_required.body'));
$this->assertSame('Soon...', $config->get('register_pending_approval.subject'));
$this->assertSame('...you will join our Circle. Let the Drupal flow through you.', $config->get('register_pending_approval.body'));
$this->assertSame('BEGONE!', $config->get('status_blocked.subject'));
$this->assertSame('You no longer please the robot overlords. Go to your room and chill out.', $config->get('status_blocked.body'));
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Migrate\d7;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
/**
* User picture entity display.
*
* @group user
*/
class MigrateUserPictureEntityDisplayTest extends MigrateDrupal7TestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['file', 'image'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('file');
$this->executeMigrations([
'user_picture_field',
'user_picture_field_instance',
'user_picture_entity_display',
]);
}
/**
* Tests the Drupal 7 user picture to Drupal 8 entity display migration.
*/
public function testUserPictureEntityDisplay(): void {
$component = EntityViewDisplay::load('user.user.default')->getComponent('user_picture');
$this->assertSame('image', $component['type']);
$this->assertSame('', $component['settings']['image_style']);
$this->assertSame('content', $component['settings']['image_link']);
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Migrate\d7;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
/**
* Tests migration of the user_picture field's entity form display settings.
*
* @group user
*/
class MigrateUserPictureEntityFormDisplayTest extends MigrateDrupal7TestBase {
protected static $modules = ['image', 'file'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->executeMigrations([
'user_picture_field',
'user_picture_field_instance',
'user_picture_entity_form_display',
]);
}
/**
* Tests the field's entity form display settings.
*/
public function testEntityFormDisplaySettings(): void {
$component = EntityFormDisplay::load('user.user.default')->getComponent('user_picture');
$this->assertSame('image_image', $component['type']);
$this->assertSame('throbber', $component['settings']['progress_indicator']);
$this->assertSame('thumbnail', $component['settings']['preview_image_style']);
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Migrate\d7;
use Drupal\Core\Field\FieldConfigInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
/**
* User picture field instance migration.
*
* @group user
*/
class MigrateUserPictureFieldInstanceTest extends MigrateDrupal7TestBase {
protected static $modules = ['image', 'file'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->executeMigrations([
'user_picture_field',
'user_picture_field_instance',
]);
}
/**
* Tests the user picture field migration.
*/
public function testUserPictureField(): void {
/** @var \Drupal\field\FieldConfigInterface $field */
$field = FieldConfig::load('user.user.user_picture');
$this->assertInstanceOf(FieldConfigInterface::class, $field);
$this->assertSame('user', $field->getTargetEntityTypeId());
$this->assertSame('user', $field->getTargetBundle());
$this->assertSame('user_picture', $field->getName());
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Migrate\d7;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\FieldStorageConfigInterface;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
/**
* User picture field migration.
*
* @group user
*/
class MigrateUserPictureFieldTest extends MigrateDrupal7TestBase {
protected static $modules = ['image', 'file'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->executeMigration('user_picture_field');
}
/**
* Tests the user picture field migration.
*/
public function testUserPictureField(): void {
/** @var \Drupal\field\FieldStorageConfigInterface $field_storage */
$field_storage = FieldStorageConfig::load('user.user_picture');
$this->assertInstanceOf(FieldStorageConfigInterface::class, $field_storage);
$this->assertSame('user.user_picture', $field_storage->id());
$this->assertSame('image', $field_storage->getType());
$this->assertSame('user', $field_storage->getTargetEntityTypeId());
}
}

View File

@@ -0,0 +1,214 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Migrate\d7;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
/**
* Upgrade user roles to user.role.*.yml.
*
* @group user
*/
class MigrateUserRoleTest extends MigrateDrupal7TestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->executeMigration('d7_user_role');
}
/**
* Asserts aspects of a user role config entity.
*
* @param string $id
* The role ID.
* @param string $label
* The role's expected label.
* @param string[] $permissions
* The expected permissions.
*
* @internal
*/
protected function assertEntity(string $id, string $label, array $permissions): void {
/** @var \Drupal\user\RoleInterface $entity */
$entity = Role::load($id);
$this->assertInstanceOf(RoleInterface::class, $entity);
$this->assertSame($label, $entity->label());
$this->assertSame($permissions, $entity->getPermissions());
}
/**
* Tests user role migration.
*/
public function testUserRole(): void {
$anonymous_permissions = ['access content'];
$this->assertEntity('anonymous', 'anonymous user', $anonymous_permissions);
$this->assertEntity('authenticated', 'authenticated user', $anonymous_permissions);
$admin_permissions = [
'access administration pages',
'access content',
'access site in maintenance mode',
'access site reports',
'access user profiles',
'administer actions',
'administer menu',
'administer modules',
'administer permissions',
'administer site configuration',
'administer software updates',
'administer themes',
'administer users',
'cancel account',
'change own username',
'select account cancellation method',
'view the administration theme',
];
$this->assertEntity('administrator', 'administrator', $admin_permissions);
// Test there are no duplicated roles.
$roles = [
'anonymous1',
'authenticated1',
'administrator1',
];
$this->assertEmpty(Role::loadMultiple($roles));
// Remove the map row for the administrator role and rerun the migration.
// This will re-import the administrator role again.
$id_map = $this->getMigration('d7_user_role')->getIdMap();
$id_map->delete(['rid' => 3]);
$this->sourceDatabase->insert('role')
->fields([
'rid' => 4,
'name' => 'test role',
'weight' => 10,
])
->execute();
$this->sourceDatabase->insert('role_permission')
->fields([
'rid' => 4,
'permission' => 'access content',
'module' => 'node',
])
->execute();
$this->executeMigration('d7_user_role');
// Test there are no duplicated roles.
$roles = [
'anonymous1',
'authenticated1',
'administrator1',
];
$this->assertEmpty(Role::loadMultiple($roles));
// Test that the existing roles have not changed.
$this->assertEntity('administrator', 'administrator', $admin_permissions);
$this->assertEntity('anonymous', 'anonymous user', $anonymous_permissions);
$this->assertEntity('authenticated', 'authenticated user', $anonymous_permissions);
// Test the migration of the new role, test role.
$this->assertEntity('test_role', 'test role', $anonymous_permissions);
// Tests the migration log contains an error message.
// User role Authenticated.
$permissions[1] = [
'access comments',
'use text format filtered_html',
];
// User role test_role.
$permissions[2] = [
'access comments',
'post comments',
'skip comment approval',
'use text format custom_text_format',
'use text format filtered_html',
];
// User role administrator.
$permissions[3] = [
'access all views',
'access comments',
'access content overview',
'access contextual links',
'access news feeds',
'access printer-friendly version',
'access site-wide contact form',
'access statistics',
'access toolbar',
'access user contact forms',
'add content to books',
'administer blocks',
'administer book outlines',
'administer comments',
'administer contact forms',
'administer content types',
'administer fields',
'administer filters',
'administer forums',
'administer image styles',
'administer languages',
'administer news feeds',
'administer nodes',
'administer search',
'administer shortcuts',
'administer statistics',
'administer taxonomy',
'administer unit tests',
'administer url aliases',
'administer views',
'block IP addresses',
'bypass node access',
'create article content',
'create new books',
'create page content',
'create url aliases',
'customize shortcut links',
'delete any article content',
'delete any page content',
'delete own article content',
'delete own page content',
'delete revisions',
'delete terms in 1',
'edit any article content',
'edit any page content',
'edit own article content',
'edit own comments',
'edit own page content',
'edit terms in 1',
'post comments',
'revert revisions',
'search content',
'skip comment approval',
'switch shortcut sets',
'translate admin strings',
'translate blocks',
'translate content',
'translate interface',
'translate user-defined strings',
'use PHP for settings',
'use advanced search',
'use text format custom_text_format',
'use text format filtered_html',
'use text format full_html',
'view own unpublished content',
'view post access counter',
'view revisions',
];
foreach ($id_map->getMessages() as $message) {
$expected_permissions = implode("', '", $permissions[$message->src_rid]);
$expected_message = "Permission(s) '" . $expected_permissions . "' not found.";
$this->assertSame($expected_message, $message->message);
$this->assertSame(MigrationInterface::MESSAGE_WARNING, (int) $message->level);
}
$this->assertSame(3, $id_map->messageCount());
}
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Migrate\d7;
use Drupal\Tests\SchemaCheckTestTrait;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
use Drupal\user\AccountSettingsForm;
use Drupal\Core\Database\Database;
use Drupal\user\UserInterface;
/**
* Tests migration of user settings.
*
* @group migrate_drupal_7
*/
class MigrateUserSettingsTest extends MigrateDrupal7TestBase {
use SchemaCheckTestTrait;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->executeMigrations(['d7_user_settings']);
}
/**
* Tests the migration.
*/
public function testMigration(): void {
$config = $this->config('user.settings');
$this->assertTrue($config->get('notify.status_blocked'));
$this->assertTrue($config->get('notify.status_activated'));
$this->assertTrue($config->get('verify_mail'));
$this->assertSame(UserInterface::REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL, $config->get('register'));
$this->assertSame('Anonymous', $config->get('anonymous'));
// Tests migration of user_register using the AccountSettingsForm.
// Map source values to destination values.
$user_register_map = [
[0, UserInterface::REGISTER_ADMINISTRATORS_ONLY],
[1, UserInterface::REGISTER_VISITORS],
[2, UserInterface::REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL],
];
foreach ($user_register_map as $map) {
// Tests migration of user_register = 1.
Database::getConnection('default', 'migrate')
->update('variable')
->fields(['value' => serialize($map[0])])
->condition('name', 'user_register')
->execute();
/** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
$migration = $this->getMigration('d7_user_settings');
// Indicate we're rerunning a migration that's already run.
$migration->getIdMap()->prepareUpdate();
$this->executeMigration($migration);
$form = $this->container->get('form_builder')->getForm(AccountSettingsForm::create($this->container));
$this->assertSame($map[1], $form['registration_cancellation']['user_register']['#value']);
}
}
}

View File

@@ -0,0 +1,265 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Migrate\d7;
use Drupal\Core\Database\Database;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
use Drupal\user\Entity\User;
use Drupal\user\RoleInterface;
use Drupal\user\UserInterface;
/**
* Users migration.
*
* @group user
*/
class MigrateUserTest extends MigrateDrupal7TestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'comment',
'content_translation',
'datetime',
'datetime_range',
'image',
'language',
'link',
'menu_ui',
'node',
'phpass',
'taxonomy',
'telephone',
'text',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('comment');
$this->installEntitySchema('taxonomy_term');
$this->executeMigration('language');
$this->migrateFields();
$this->migrateUsers();
$this->executeMigrations([
'd7_entity_translation_settings',
'd7_user_entity_translation',
]);
}
/**
* Asserts various aspects of a user account.
*
* @param string $id
* The user ID.
* @param string $label
* The username.
* @param string $mail
* The user's email address.
* @param string $password
* The password for this user.
* @param int $created
* The user's creation time.
* @param int $access
* The last access time.
* @param int $login
* The last login time.
* @param bool $blocked
* Whether or not the account is blocked.
* @param string $entity_langcode
* The user entity language code.
* @param string $prefered_langcode
* The user prefered language code.
* @param string $timezone
* The user account's timezone name.
* @param string $init
* The user's initial email address.
* @param string[] $roles
* Role IDs the user account is expected to have.
* @param array|null $field_integer
* The value of the integer field.
* @param int|false $field_file_target_id
* (optional) The target ID of the file field.
* @param bool $has_picture
* (optional) Whether the user is expected to have a picture attached.
*
* @internal
*/
protected function assertEntity(string $id, string $label, string $mail, string $password, int $created, int $access, int $login, bool $blocked, string $entity_langcode, string $prefered_langcode, string $timezone, string $init, array $roles, ?array $field_integer, $field_file_target_id = FALSE, bool $has_picture = FALSE): void {
/** @var \Drupal\user\UserInterface $user */
$user = User::load($id);
$this->assertInstanceOf(UserInterface::class, $user);
$this->assertSame($label, $user->label());
$this->assertSame($mail, $user->getEmail());
$this->assertSame($password, $user->getPassword());
$this->assertSame($created, (int) $user->getCreatedTime());
$this->assertSame($access, (int) $user->getLastAccessedTime());
$this->assertSame($login, (int) $user->getLastLoginTime());
$this->assertNotSame($blocked, (bool) $user->isBlocked());
// Ensure the user's langcode, preferred_langcode and
// preferred_admin_langcode are valid.
// $user->getPreferredLangcode() might fallback to default language if the
// user preferred language is not configured on the site. We just want to
// test if the value was imported correctly.
$language_manager = $this->container->get('language_manager');
$default_langcode = $language_manager->getDefaultLanguage()->getId();
if ($prefered_langcode == '') {
$this->assertSame('en', $user->langcode->value);
$this->assertSame($default_langcode, $user->preferred_langcode->value);
$this->assertSame($default_langcode, $user->preferred_admin_langcode->value);
}
elseif ($language_manager->getLanguage($prefered_langcode) === NULL) {
$this->assertSame($default_langcode, $user->langcode->value);
$this->assertSame($default_langcode, $user->preferred_langcode->value);
$this->assertSame($default_langcode, $user->preferred_admin_langcode->value);
}
else {
$this->assertSame($entity_langcode, $user->langcode->value);
$this->assertSame($prefered_langcode, $user->preferred_langcode->value);
$this->assertSame($prefered_langcode, $user->preferred_admin_langcode->value);
}
$this->assertSame($timezone, $user->getTimeZone());
$this->assertSame($init, $user->getInitialEmail());
$this->assertSame($roles, $user->getRoles());
$this->assertSame($has_picture, !$user->user_picture->isEmpty());
if (!is_null($field_integer)) {
$this->assertTrue($user->hasField('field_integer'));
$this->assertEquals($field_integer[0], $user->field_integer->value);
}
if (!empty($field_file_target_id)) {
$this->assertTrue($user->hasField('field_file'));
$this->assertSame($field_file_target_id, $user->field_file->target_id);
}
}
/**
* Tests the Drupal 7 user to Drupal 8 migration.
*/
public function testUser(): void {
$users = Database::getConnection('default', 'migrate')
->select('users', 'u')
->fields('u')
->condition('uid', 1, '>')
->execute()
->fetchAll();
foreach ($users as $source) {
$rids = Database::getConnection('default', 'migrate')
->select('users_roles', 'ur')
->fields('ur', ['rid'])
->condition('ur.uid', $source->uid)
->execute()
->fetchCol();
$roles = [RoleInterface::AUTHENTICATED_ID];
$id_map = $this->getMigration('d7_user_role')->getIdMap();
foreach ($rids as $rid) {
$role = $id_map->lookupDestinationIds([$rid])[0];
$roles[] = reset($role);
}
$entity_translation = Database::getConnection('default', 'migrate')
->select('entity_translation', 'et')
->fields('et', ['language'])
->condition('et.entity_type', 'user')
->condition('et.entity_id', $source->uid)
->condition('et.source', '')
->execute()
->fetchField();
$entity_language = $entity_translation ?: $source->language;
$field_integer = Database::getConnection('default', 'migrate')
->select('field_data_field_integer', 'fi')
->fields('fi', ['field_integer_value'])
->condition('fi.entity_id', $source->uid)
->condition('fi.language', $entity_language)
->execute()
->fetchCol();
$field_integer = !empty($field_integer) ? $field_integer : NULL;
$field_file = Database::getConnection('default', 'migrate')
->select('field_data_field_file', 'ff')
->fields('ff', ['field_file_fid'])
->condition('ff.entity_id', $source->uid)
->execute()
->fetchField();
$this->assertEntity(
$source->uid,
$source->name,
$source->mail,
$source->pass,
(int) $source->created,
(int) $source->access,
(int) $source->login,
(bool) $source->status,
$entity_language,
$source->language,
$source->timezone,
$source->init,
$roles,
$field_integer,
$field_file
);
// Ensure that the user can authenticate.
$this->assertEquals($source->uid, $this->container->get('user.auth')->authenticate($source->name, 'a password'));
// After authenticating the password will be rehashed because the password
// stretching iteration count has changed from 15 in Drupal 7 to 16 in
// Drupal 8.
$user = User::load($source->uid);
$rehash = $user->getPassword();
$this->assertNotEquals($source->pass, $rehash);
// Authenticate again and there should be no re-hash.
$this->assertEquals($source->uid, $this->container->get('user.auth')->authenticate($source->name, 'a password'));
$user = User::load($source->uid);
$this->assertEquals($rehash, $user->getPassword());
}
// Tests the Drupal 7 user entity translations to Drupal 8 migration.
$manager = $this->container->get('content_translation.manager');
// Get the user and its translations.
$user = User::load(2);
$user_fr = $user->getTranslation('fr');
$user_is = $user->getTranslation('is');
// Test that fields translated with Entity Translation are migrated.
$this->assertSame('99', $user->field_integer->value);
$this->assertSame('9', $user_fr->field_integer->value);
$this->assertSame('1', $user_is->field_integer->value);
// Test that the French translation metadata is correctly migrated.
$metadata_fr = $manager->getTranslationMetadata($user_fr);
$this->assertSame('en', $metadata_fr->getSource());
$this->assertSame('1', $metadata_fr->getAuthor()->uid->value);
$this->assertSame('1531663916', $metadata_fr->getCreatedTime());
$this->assertFalse($metadata_fr->isOutdated());
$this->assertFalse($metadata_fr->isPublished());
// Test that the Icelandic translation metadata is correctly migrated.
$metadata_is = $manager->getTranslationMetadata($user_is);
$this->assertSame('en', $metadata_is->getSource());
$this->assertSame('2', $metadata_is->getAuthor()->uid->value);
$this->assertSame('1531663925', $metadata_is->getCreatedTime());
$this->assertTrue($metadata_is->isOutdated());
$this->assertTrue($metadata_is->isPublished());
// Test that untranslatable properties are the same as the source language.
$this->assertSame($user->label(), $user_fr->label());
$this->assertSame($user->label(), $user_is->label());
$this->assertSame($user->getEmail(), $user_fr->getEmail());
$this->assertSame($user->getEmail(), $user_is->getEmail());
$this->assertSame($user->getPassword(), $user_fr->getPassword());
$this->assertSame($user->getPassword(), $user_is->getPassword());
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Migrate\d7;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
use Drupal\migrate\Exception\RequirementsException;
/**
* Tests check requirements for profile_field source plugin.
*
* @group user
*/
class ProfileFieldCheckRequirementsTest extends MigrateDrupal7TestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->sourceDatabase->schema()->dropTable('profile_field');
}
/**
* Tests exception is thrown when profile_fields tables do not exist.
*/
public function testCheckRequirements(): void {
$this->expectException(RequirementsException::class);
$this->expectExceptionMessage('Profile module not enabled on source site');
$this->getMigration('user_profile_field')
->getSourcePlugin()
->checkRequirements();
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Migrate\d7;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
/**
* Tests the user migration plugin class.
*
* @group user
*/
class UserMigrationClassTest extends MigrateDrupal7TestBase {
/**
* Tests that the profile value process is added to the pipeline.
*
* Ensures profile fields are merged into the d7_profile_values migration's
* process pipeline.
*/
public function testClass(): void {
$migration = $this->getMigration('d7_user');
/** @var \Drupal\migrate\Plugin\MigrationInterface[] $migrations */
$this->assertSame('d7_user', $migration->id());
$process = $migration->getProcess();
$this->assertSame('field_file', $process['field_file'][0]['source']);
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
/**
* Tests the ModulePermissionsLinkHelper.
*
* @group user
* @coversDefaultClass \Drupal\user\ModulePermissionsLinkHelper
*/
class ModulePermissionsLinkHelperTest extends KernelTestBase {
use UserCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'user',
'user_permissions_test',
];
/**
* {@inheritdoc}
*/
public function setUp(): void {
parent::setUp();
$this->setUpCurrentUser([], [
'administer permissions',
]);
}
/**
* @covers ::getModulePermissionsLink
*/
public function testGetModulePermissionsLink(): void {
/** @var \Drupal\user\ModulePermissionsLinkHelper $permsLinkHelper */
$permsLinkHelper = $this->container->get('user.module_permissions_link_helper');
$permsLink = $permsLinkHelper->getModulePermissionsLink('user_permissions_test', 'User permissions test');
$this->assertNotEmpty($permsLink);
$this->assertEquals("Configure User permissions test permissions", $permsLink['title']);
/** @var \Drupal\Core\Url $url */
$url = $permsLink['url'];
$this->assertEquals('user.admin_permissions.module', $url->getRouteName());
$this->assertEquals('user_permissions_test', $url->getRouteParameters()['modules']);
}
}

View File

@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Plugin\migrate\source;
use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
/**
* Tests the profile_field source plugin.
*
* @covers \Drupal\user\Plugin\migrate\source\ProfileField
* @group user
*/
class ProfileFieldTest extends MigrateSqlSourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['user', 'migrate_drupal'];
/**
* {@inheritdoc}
*/
public static function providerSource() {
$tests = [
[
'source_data' => [],
'expected_data' => [],
],
];
$profile_fields = [
[
'fid' => 1,
'title' => 'First name',
'name' => 'profile_first_name',
'explanation' => 'First name user',
'category' => 'profile',
'page' => '',
'type' => 'textfield',
'weight' => 0,
'required' => 1,
'register' => 0,
'visibility' => 2,
'autocomplete' => 0,
'options' => '',
],
[
'fid' => 2,
'title' => 'Last name',
'name' => 'profile_last_name',
'explanation' => 'Last name user',
'category' => 'profile',
'page' => '',
'type' => 'textfield',
'weight' => 0,
'required' => 0,
'register' => 0,
'visibility' => 2,
'autocomplete' => 0,
'options' => '',
],
[
'fid' => 3,
'title' => 'Policy',
'name' => 'profile_policy',
'explanation' => 'A checkbox that say if you accept policy of website',
'category' => 'profile',
'page' => '',
'type' => 'checkbox',
'weight' => 0,
'required' => 1,
'register' => 1,
'visibility' => 2,
'autocomplete' => 0,
'options' => '',
],
[
'fid' => 4,
'title' => 'Color',
'name' => 'profile_color',
'explanation' => 'A selection that allows user to select a color',
'category' => 'profile',
'page' => '',
'type' => 'selection',
'weight' => 0,
'required' => 0,
'register' => 0,
'visibility' => 2,
'autocomplete' => 0,
'options' => "red\nblue\ngreen",
],
];
$tests[0]['source_data']['profile_fields'] = $profile_fields;
// Profile values are merged with pre-set options of a "selection" field.
$tests[0]['source_data']['profile_values'] = [
[
'fid' => 4,
'uid' => 1,
'value' => 'yellow',
],
];
// Expected options are:
// - for "checkbox" fields - array with NULL options.
// - for "selection" fields - options in both keys and values.
$expected_field_options = [
'',
'',
[NULL, NULL],
[
'red' => 'red',
'blue' => 'blue',
'green' => 'green',
'yellow' => 'yellow',
],
];
$tests[0]['expected_data'] = $profile_fields;
foreach ($tests[0]['expected_data'] as $delta => $row) {
$tests[0]['expected_data'][$delta]['options'] = $expected_field_options[$delta];
}
return $tests;
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Plugin\migrate\source;
use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
/**
* Tests the user_picture_instance source plugin.
*
* @covers \Drupal\user\Plugin\migrate\source\UserPictureInstance
* @group user
*/
class UserPictureInstanceTest extends MigrateSqlSourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['user', 'migrate_drupal'];
/**
* {@inheritdoc}
*/
public static function providerSource() {
$tests = [];
// The source data.
$tests[0]['source_data']['variable'] = [
[
'name' => 'file_directory',
'value' => serialize(NULL),
],
[
'name' => 'user_picture_file_size',
'value' => serialize(128),
],
[
'name' => 'user_picture_dimensions',
'value' => serialize('128x128'),
],
];
// The expected results.
$tests[0]['expected_data'] = [
[
'id' => '',
'file_directory' => 'pictures',
'max_filesize' => '128KB',
'max_resolution' => '128x128',
],
];
return $tests;
}
}

View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Plugin\migrate\source\d6;
use Drupal\Tests\user\Kernel\Plugin\migrate\source\ProfileFieldTest;
// cspell:ignore objectid
/**
* Tests the field option translation source plugin.
*
* @covers \Drupal\user\Plugin\migrate\source\d6\ProfileFieldOptionTranslation
* @group migrate_drupal
*/
class ProfileFieldOptionTranslationTest extends ProfileFieldTest {
/**
* {@inheritdoc}
*/
protected static $modules = ['user', 'migrate_drupal'];
/**
* {@inheritdoc}
*/
public static function providerSource() {
$test = parent::providerSource();
// The source data.
$test[0]['source_data']['i18n_strings'] = [
[
'lid' => 10,
'objectid' => 'profile_color',
'type' => 'field',
'property' => 'options',
],
[
'lid' => 1,
'objectid' => 'profile_last_name',
'type' => 'field',
'property' => 'options',
],
];
$test[0]['source_data']['locales_target'] = [
[
'lid' => 10,
'translation' => "fr - red\nfr - blue\nfr - green",
'language' => 'fr',
],
];
$test[0]['expected_data'] = [
[
'fid' => 4,
'title' => 'Color',
'name' => 'profile_color',
'explanation' => 'A selection that allows user to select a color',
'category' => 'profile',
'page' => '',
'type' => 'selection',
'weight' => 0,
'required' => 0,
'register' => 0,
'visibility' => 2,
'autocomplete' => 0,
'options' => [
'red' => 'red',
'blue' => 'blue',
'green' => 'green',
'yellow' => 'yellow',
],
'property' => 'options',
'objectid' => 'profile_color',
'translation' => "fr - red\nfr - blue\nfr - green",
'language' => 'fr',
],
];
return $test;
}
}

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Plugin\migrate\source\d6;
use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
/**
* Tests the d6_profile_field_values source plugin.
*
* @covers \Drupal\user\Plugin\migrate\source\d6\ProfileFieldValues
* @group user
*/
class ProfileFieldValuesTest extends MigrateSqlSourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['user', 'migrate_drupal'];
/**
* {@inheritdoc}
*/
public static function providerSource() {
$tests = [];
// The source data.
$tests[0]['source_data']['profile_values'] = [
[
'fid' => '8',
'uid' => '2',
'value' => 'red',
],
[
'fid' => '9',
'uid' => '2',
'value' => 'The quick brown fox ...',
],
];
$tests[0]['source_data']['profile_fields'] = [
[
'fid' => '8',
'title' => 'Favorite color',
'name' => 'profile_color',
'explanation' => 'List your favorite color',
'category' => 'Personal information',
'page' => 'People whose favorite color is %value',
'type' => 'textfield',
'weight' => '-10',
'required' => '0',
'register' => '1',
'visibility' => '2',
'autocomplete' => '1',
'options' => '',
],
[
'fid' => '9',
'title' => 'Biography',
'name' => 'profile_biography',
'explanation' => 'Tell people a little bit about yourself',
'category' => 'Personal information',
'page' => '',
'type' => 'textarea',
'weight' => '-8',
'required' => '0',
'register' => '0',
'visibility' => '2',
'autocomplete' => '0',
'options' => '',
],
];
// The expected results.
$tests[0]['expected_data'] = [
[
'profile_color' => ['red'],
'profile_biography' => ['The quick brown fox ...'],
'uid' => '2',
],
];
return $tests;
}
}

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\user\Kernel\Plugin\migrate\source\d6;
use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
/**
* Tests the d6_user_role source plugin.
*
* @covers \Drupal\user\Plugin\migrate\source\d6\Role
* @group user
*/
class RoleTest extends MigrateSqlSourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['user', 'migrate_drupal'];
/**
* {@inheritdoc}
*/
public static function providerSource() {
$tests = [
[
'source_data' => [],
'expected_data' => [],
],
];
$roles = [
[
'rid' => 1,
'name' => 'anonymous user',
'permissions' => [
'access content',
],
],
[
'rid' => 2,
'name' => 'authenticated user',
'permissions' => [
'access comments',
'access content',
'post comments',
'post comments without approval',
],
],
[
'rid' => 3,
'name' => 'administrator',
'permissions' => [
'access comments',
'administer comments',
'post comments',
'post comments without approval',
'access content',
'administer content types',
'administer nodes',
],
],
];
// The source data.
foreach ($roles as $role) {
$tests[0]['source_data']['permission'][] = [
'rid' => $role['rid'],
'perm' => implode(', ', $role['permissions']),
];
unset($role['permissions']);
$tests[0]['source_data']['role'][] = $role;
}
$tests[0]['source_data']['filter_formats'] = [
[
'format' => 1,
'roles' => '',
],
];
// The expected results.
$tests[0]['expected_data'] = $roles;
return $tests;
}
}

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