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,10 @@
name: 'User access tests'
type: module
description: 'Support module for user access testing.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,66 @@
<?php
/**
* @file
* Dummy module implementing hook_user_access() to test if entity access is respected.
*/
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\user\Entity\User;
/**
* Implements hook_ENTITY_TYPE_access() for entity type "user".
*/
function user_access_test_user_access(User $entity, $operation, $account) {
if ($entity->getAccountName() == "no_edit" && $operation == "update") {
// Deny edit access.
return AccessResult::forbidden();
}
if ($entity->getAccountName() == "no_delete" && $operation == "delete") {
// Deny delete access.
return AccessResult::forbidden();
}
// Account with role sub-admin can manage users with no roles.
if (count($entity->getRoles()) == 1) {
return AccessResult::allowedIfHasPermission($account, 'sub-admin');
}
return AccessResult::neutral();
}
/**
* Implements hook_entity_create_access().
*/
function user_access_test_entity_create_access(AccountInterface $account, array $context, $entity_bundle) {
if ($context['entity_type_id'] != 'user') {
return AccessResult::neutral();
}
// Account with role sub-admin can create users.
return AccessResult::allowedIfHasPermission($account, 'sub-admin');
}
/**
* Implements hook_entity_field_access().
*/
function user_access_test_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
// Account with role sub-admin can view the status, init and mail fields for
// user with no roles.
if ($field_definition->getTargetEntityTypeId() == 'user' && $operation === 'view' && in_array($field_definition->getName(), ['status', 'init', 'mail'])) {
if (($items == NULL) || (count($items->getEntity()->getRoles()) == 1)) {
return AccessResult::allowedIfHasPermission($account, 'sub-admin');
}
}
if (\Drupal::state()->get('user_access_test_forbid_mail_edit', FALSE)) {
if ($operation === 'edit' && $items && $items->getEntity()->getEntityTypeId() === 'user' && $field_definition->getName() === 'mail') {
return AccessResult::forbidden();
}
}
return AccessResult::neutral();
}

View File

@@ -0,0 +1,2 @@
sub-admin:
title: 'Administer users with no roles'

View File

@@ -0,0 +1,10 @@
name: 'User custom password hash params test'
type: module
description: 'Support module for testing custom hashing password algorithm parameters.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,19 @@
services:
# The first argument of the hashing service (constructor of PhpPassword) is
# the hashing algorithm. This should be set to PASSWORD_DEFAULT for hash
# params test (In PHP 8, PASSWORD_DEFAULT equals PASSWORD_BCRYPT).
#
# The second argument of the hashing service (constructor of PhpPassword)
# specifies the options passed to password_hash(). In PHP 8 the default 'cost'
# value is 10. For the hash parameter test, the value must be higher than the
# default value.
#
# Future versions of PHP may increase this value in order to counteract
# increases in the speed and power of computers available to crack the hashes.
# It is necessary to track changes of the default options when new versions
# of PHP are released and increment the cost parameter accordingly.
password:
class: Drupal\Core\Password\PhpPassword
arguments:
- "2y"
- { cost: 11 }

View File

@@ -0,0 +1,10 @@
name: 'User module form tests'
type: module
description: 'Support module for user form testing.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,14 @@
<?php
/**
* @file
* Support module for user form testing.
*/
/**
* Implements hook_form_FORM_ID_alter() for user_cancel_form().
*/
function user_form_test_form_user_cancel_form_alter(&$form, &$form_state) {
$form['user_cancel_confirm']['#default_value'] = FALSE;
$form['access']['#value'] = \Drupal::currentUser()->hasPermission('cancel other accounts');
}

View File

@@ -0,0 +1,2 @@
cancel other accounts:
title: 'Cancel other user accounts'

View File

@@ -0,0 +1,6 @@
user_form_test.cancel:
path: '/user_form_test_cancel/{user}'
defaults:
_entity_form: 'user.cancel'
requirements:
_permission: 'cancel other accounts'

View File

@@ -0,0 +1,10 @@
name: 'User module hooks tests'
type: module
description: 'Support module for user hooks testing.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,23 @@
<?php
/**
* @file
* Support module for user hooks testing.
*/
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Session\AccountInterface;
/**
* Implements hook_user_format_name_alter().
*/
function user_hooks_test_user_format_name_alter(&$name, AccountInterface $account) {
if (\Drupal::state()->get('user_hooks_test_user_format_name_alter', FALSE)) {
if (\Drupal::state()->get('user_hooks_test_user_format_name_alter_safe', FALSE)) {
$name = new FormattableMarkup('<em>@uid</em>', ['@uid' => $account->id()]);
}
else {
$name = '<em>' . $account->id() . '</em>';
}
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Drupal\user_language_test\Controller;
/**
* Returns responses for User Language Test routes.
*/
class UserLanguageTestController {
/**
* Builds the response.
*/
public function buildPostResponse() {
return ['#markup' => 'It works!'];
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Drupal\user_language_test\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Provides a User Language Test form.
*/
class UserLanguageTestForm extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'user_language_test';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['#action'] = Url::fromRoute('user_language_test.post_response')->toString();
$form['actions'] = [
'#type' => 'actions',
'submit' => [
'#type' => 'submit',
'#value' => $this->t('Send'),
],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
}
}

View File

@@ -0,0 +1,10 @@
name: 'User language tests'
type: module
description: 'Support module for user language testing.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,16 @@
user_language_test.post_response:
path: '/user-language-test/post'
defaults:
_controller: Drupal\user_language_test\Controller\UserLanguageTestController::buildPostResponse
methods: [post]
options:
_admin_route: TRUE
requirements:
_access: 'TRUE'
user_language_test.form:
path: '/user-language-test/form'
defaults:
_form: 'Drupal\user_language_test\Form\UserLanguageTestForm'
requirements:
_access: 'TRUE'

View File

@@ -0,0 +1,10 @@
name: 'User permission tests'
type: module
description: 'Support module for user permission testing.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,6 @@
c:
title: 'Test permission'
a:
title: 'Test permission'
b:
title: 'Test permission'

View File

@@ -0,0 +1,33 @@
langcode: en
status: true
dependencies:
module:
- user
id: test_access_perm
label: test_access_perm
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
display:
default:
display_options:
access:
type: perm
options:
perm: 'views_test_data test permission'
cache:
type: tag
exposed_form:
type: basic
pager:
type: full
style:
type: default
row:
type: fields
display_plugin: default
display_title: Default
id: default
position: 0

View File

@@ -0,0 +1,44 @@
langcode: en
status: true
dependencies:
module:
- user
id: test_access_role
label: test_access_role
module: views
description: ''
tag: ''
base_table: views_test_data
base_field: nid
display:
default:
display_options:
fields:
id:
id: id
field: id
table: views_test_data
plugin_id: numeric
access:
type: role
cache:
type: tag
exposed_form:
type: basic
pager:
type: full
style:
type: default
row:
type: fields
display_plugin: default
display_title: Default
id: default
position: 0
page_1:
display_options:
path: test-role
display_plugin: page
display_title: Page
id: page_1
position: 1

View File

@@ -0,0 +1,138 @@
langcode: en
status: true
dependencies:
module:
- user
id: test_field_permission
label: test_node_revision_vid
module: views
description: ''
tag: ''
base_table: users_field_data
base_field: uid
display:
default:
display_plugin: default
id: default
display_title: Default
position: null
display_options:
access:
type: none
cache:
type: tag
query:
type: views_query
exposed_form:
type: basic
pager:
type: full
style:
type: default
row:
type: fields
fields:
uid:
id: uid
table: users_field_data
field: uid
relationship: none
group_type: group
admin_label: ''
label: Uid
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
plugin_id: field
entity_type: user
entity_field: uid
permission:
id: permission
table: user__roles
field: permission
relationship: none
group_type: group
admin_label: ''
label: Permission
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
type: separator
separator: ', '
plugin_id: user_permissions
filters: { }
sorts: { }

View File

@@ -0,0 +1,132 @@
langcode: en
status: true
dependencies:
module:
- user
id: test_filter_current_user
label: Users
module: views
description: ''
tag: ''
base_table: users_field_data
base_field: uid
display:
default:
display_plugin: default
id: default
display_title: Default
position: 0
display_options:
access:
type: none
cache:
type: tag
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
exposed_form:
type: basic
options:
submit_button: Filter
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: none
options:
offset: 0
style:
type: default
options:
row_class: ''
default_row_class: true
uses_fields: false
row:
type: fields
options:
separator: ''
hide_empty: false
default_field_elements: true
fields:
uid:
id: uid
table: users
field: uid
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
filters:
uid_current:
id: uid_current
table: users
field: uid_current
relationship: none
group_type: group
admin_label: ''
operator: '='
value: '1'
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: user
plugin_id: user_current
sorts:
uid:
id: uid
table: users_field_data
field: uid
relationship: none
group_type: group
admin_label: ''
order: ASC
exposed: false
expose:
label: ''
entity_type: user
entity_field: uid
plugin_id: standard
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- user
tags: { }

View File

@@ -0,0 +1,141 @@
langcode: en
status: true
dependencies:
module:
- user
id: test_filter_permission
label: test_filter_permission
module: views
description: ''
tag: ''
base_table: users_field_data
base_field: uid
display:
default:
display_plugin: default
id: default
display_title: Default
position: null
display_options:
access:
type: none
cache:
type: tag
query:
type: views_query
exposed_form:
type: basic
pager:
type: full
style:
type: default
row:
type: fields
fields:
uid:
id: uid
table: users
field: uid
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
link_to_user: false
plugin_id: user
entity_type: user
entity_field: uid
filters:
permission:
id: permission
table: user__roles
field: permission
relationship: none
group_type: group
admin_label: ''
operator: or
value:
'access user profiles': 'access user profiles'
group: 1
exposed: false
expose:
operator_id: '0'
label: ''
description: ''
use_operator: false
operator: ''
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
reduce: false
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
reduce_duplicates: true
plugin_id: user_permissions
sorts:
uid:
id: uid
table: users_field_data
field: uid
relationship: none
group_type: group
admin_label: ''
order: ASC
exposed: false
expose:
label: ''
plugin_id: standard
entity_type: user
entity_field: uid

View File

@@ -0,0 +1,92 @@
langcode: en
status: true
dependencies:
module:
- node
- user
id: test_groupwise_user
label: test_groupwise_user
module: views
description: ''
tag: default
base_table: users_field_data
base_field: uid
display:
default:
display_options:
access:
options:
perm: 'access user profiles'
type: perm
cache:
type: tag
exposed_form:
type: basic
fields:
name:
field: name
id: name
table: users_field_data
plugin_id: field
type: user_name
entity_type: user
entity_field: name
nid:
field: nid
id: nid
relationship: uid_representative
table: node_field_data
plugin_id: node
entity_type: node
entity_field: nid
filters:
status:
expose:
operator: '0'
field: status
group: 1
id: status
table: users_field_data
value: '1'
plugin_id: boolean
entity_type: user
entity_field: status
pager:
options:
items_per_page: 10
type: full
query:
type: views_query
relationships:
uid_representative:
admin_label: 'Representative node'
field: uid_representative
group_type: group
id: uid_representative
relationship: none
required: false
subquery_namespace: ''
subquery_order: DESC
subquery_regenerate: true
subquery_sort: node.nid
subquery_view: ''
table: users_field_data
plugin_id: groupwise_max
row:
type: fields
sorts:
created:
field: uid
id: uid
order: ASC
table: users_field_data
plugin_id: field
entity_type: user
entity_field: uid
style:
type: default
title: test_groupwise_user
display_plugin: default
display_title: Default
id: default
position: 0

View File

@@ -0,0 +1,62 @@
langcode: en
status: true
dependencies:
module:
- node
id: test_plugin_argument_default_current_user
label: test_plugin_argument_default_current_user
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
display:
default:
display_options:
access:
type: none
arguments:
'null':
default_action: default
default_argument_type: current_user
field: 'null'
id: 'null'
must_not_be: false
table: views
plugin_id: 'null'
cache:
type: tag
exposed_form:
type: basic
fields:
title:
alter:
alter_text: false
ellipsis: true
html: false
make_link: false
strip_tags: false
trim: false
word_boundary: true
empty_zero: false
field: title
hide_empty: false
id: title
table: node_field_data
plugin_id: field
entity_type: node
entity_field: title
pager:
options:
id: 0
items_per_page: 10
offset: 0
type: full
style:
type: default
row:
type: fields
display_plugin: default
display_title: Default
id: default
position: 0

View File

@@ -0,0 +1,64 @@
langcode: en
status: true
dependencies:
module:
- user
id: test_user_bulk_form
label: test_user_bulk_form
module: views
description: ''
tag: ''
base_table: users_field_data
base_field: uid
display:
default:
display_plugin: default
id: default
display_title: Default
position: null
display_options:
style:
type: table
row:
type: fields
fields:
user_bulk_form:
id: user_bulk_form
table: users
field: user_bulk_form
plugin_id: user_bulk_form
entity_type: user
name:
id: name
table: users_field_data
field: name
plugin_id: field
type: user_name
entity_type: user
entity_field: name
sorts:
uid:
id: uid
table: users_field_data
field: uid
order: ASC
plugin_id: user
entity_type: user
entity_field: uid
filters:
status:
id: status
table: users_field_data
field: status
operator: '='
value: '1'
plugin_id: boolean
entity_type: user
entity_field: status
page_1:
display_plugin: page
id: page_1
display_title: Page
position: null
display_options:
path: test-user-bulk-form

View File

@@ -0,0 +1,242 @@
langcode: en
status: true
dependencies:
module:
- user
id: test_user_bulk_form_combine_filter
label: test_user_bulk_form_combine_filter
module: views
description: ''
tag: ''
base_table: users_field_data
base_field: uid
display:
default:
display_plugin: default
id: default
display_title: Default
position: 0
display_options:
access:
type: none
options: { }
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: full
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous: ' Previous'
next: 'Next '
first: '« First'
last: 'Last »'
quantity: 9
style:
type: default
row:
type: fields
fields:
user_bulk_form:
id: user_bulk_form
table: users
field: user_bulk_form
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
action_title: 'With selection'
include_exclude: exclude
selected_actions: { }
entity_type: user
plugin_id: user_bulk_form
name:
id: name
table: users_field_data
field: name
entity_type: user
entity_field: name
label: ''
alter:
alter_text: false
make_link: false
absolute: false
trim: false
word_boundary: false
ellipsis: false
strip_tags: false
html: false
hide_empty: false
empty_zero: false
plugin_id: field
relationship: none
group_type: group
admin_label: ''
exclude: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_alter_empty: true
click_sort_column: value
type: user_name
settings: { }
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
filters:
combine:
id: combine
table: views
field: combine
relationship: none
group_type: group
admin_label: ''
operator: contains
value: dummy
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
fields:
user_bulk_form: user_bulk_form
name: name
plugin_id: combine
sorts: { }
title: test_user_bulk_form_combine_filter
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
filter_groups:
operator: AND
groups:
1: AND
cache_metadata:
max-age: 0
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url.query_args
tags: { }
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
display_extenders: { }
path: test-user-bulk-form-combine-filter
cache_metadata:
max-age: 0
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url.query_args
tags: { }

View File

@@ -0,0 +1,59 @@
langcode: en
status: true
dependencies:
module:
- user
id: test_user_changed
label: test_user_changed
module: views
description: ''
tag: ''
base_table: users_field_data
base_field: nid
display:
default:
display_options:
access:
type: none
cache:
type: tag
exposed_form:
type: basic
pager:
type: full
row:
type: fields
style:
type: default
fields:
name:
id: uid
table: users_field_data
field: uid
entity_type: user
entity_field: uid
changed:
id: changed
table: users_field_data
field: changed
label: 'Updated date'
plugin_id: field
type: timestamp
settings:
date_format: html_date
custom_date_format: ''
timezone: ''
entity_type: user
entity_field: changed
filters: { }
display_plugin: default
display_title: Default
id: default
position: 0
page_1:
display_options:
path: test_user_changed
display_plugin: page
display_title: Page
id: page_1
position: 0

View File

@@ -0,0 +1,130 @@
langcode: en
status: true
dependencies:
module:
- user
id: test_user_data
label: test_user_data
module: views
description: ''
tag: ''
base_table: users_field_data
base_field: uid
display:
default:
display_plugin: default
id: default
display_title: Default
position: null
display_options:
access:
type: perm
options:
perm: 'access user profiles'
cache:
type: tag
query:
type: views_query
exposed_form:
type: basic
pager:
type: full
style:
type: default
row:
type: fields
fields:
name:
id: name
table: users_field_data
field: name
label: ''
alter:
alter_text: false
make_link: false
absolute: false
trim: false
word_boundary: false
ellipsis: false
strip_tags: false
html: false
hide_empty: false
empty_zero: false
plugin_id: field
type: user_name
entity_type: user
entity_field: name
data:
id: data
table: users
field: data
relationship: none
group_type: group
admin_label: ''
label: Data
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
data_module: views_test_config
data_name: test_value_name
plugin_id: user_data
entity_type: user
filters:
uid:
value:
value: '2'
table: users_field_data
field: uid
id: uid
expose:
operator: '0'
group: 1
plugin_id: numeric
entity_type: user
entity_field: uid
sorts:
created:
id: created
table: users_field_data
field: created
order: DESC
plugin_id: date
entity_type: user
entity_field: created

View File

@@ -0,0 +1,477 @@
langcode: en
status: true
dependencies:
module:
- user
id: test_user_fields_access
label: test_user_fields_access
module: views
description: ''
tag: ''
base_table: users_field_data
base_field: uid
display:
default:
display_plugin: default
id: default
display_title: Default
position: 0
display_options:
access:
type: none
options: { }
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: mini
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous:
next:
style:
type: table
options:
grouping: { }
row_class: ''
default_row_class: true
override: true
sticky: false
caption: ''
summary: ''
description: ''
columns:
name: name
status: status
mail: mail
init: init
created: created
info:
name:
sortable: false
default_sort_order: asc
align: ''
separator: ''
empty_column: false
responsive: ''
status:
sortable: false
default_sort_order: asc
align: ''
separator: ''
empty_column: false
responsive: ''
mail:
sortable: false
default_sort_order: asc
align: ''
separator: ''
empty_column: false
responsive: ''
init:
sortable: false
default_sort_order: asc
align: ''
separator: ''
empty_column: false
responsive: ''
created:
sortable: false
default_sort_order: asc
align: ''
separator: ''
empty_column: false
responsive: ''
default: '-1'
empty_table: false
row:
type: fields
fields:
name:
id: name
table: users_field_data
field: name
relationship: none
group_type: group
admin_label: ''
label: Name
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: false
ellipsis: false
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: user_name
settings:
link_to_entity: true
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: user
entity_field: name
plugin_id: field
status:
id: status
table: users_field_data
field: status
relationship: none
group_type: group
admin_label: ''
label: Status
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: boolean
settings:
format: default
format_custom_true: ''
format_custom_false: ''
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: user
entity_field: status
plugin_id: field
mail:
id: mail
table: users_field_data
field: mail
relationship: none
group_type: group
admin_label: ''
label: Email
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: basic_string
settings: { }
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: user
entity_field: mail
plugin_id: field
init:
id: init
table: users_field_data
field: init
relationship: none
group_type: group
admin_label: ''
label: Init
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: basic_string
settings: { }
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: user
entity_field: init
plugin_id: field
created:
id: created
table: users_field_data
field: created
relationship: none
group_type: group
admin_label: ''
label: Created
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: timestamp
settings:
date_format: medium
custom_date_format: ''
timezone: ''
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: user
entity_field: created
plugin_id: field
filters: { }
sorts: { }
title: ''
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
cache_metadata:
max-age: 0
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url.query_args
tags: { }
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
display_extenders: { }
path: test_user_fields_access
cache_metadata:
max-age: 0
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url.query_args
tags: { }

View File

@@ -0,0 +1,61 @@
langcode: en
status: true
dependencies:
module:
- user
id: test_user_name
label: test_user_name
module: views
description: ''
tag: ''
base_table: users_field_data
base_field: nid
display:
default:
display_options:
access:
type: none
cache:
type: tag
exposed_form:
type: basic
pager:
type: full
row:
type: fields
style:
type: default
fields:
name:
id: uid
table: users_field_data
field: uid
entity_type: user
entity_field: uid
filters:
uid:
id: uid
table: users_field_data
field: uid
exposed: true
expose:
operator_id: uid_op
label: Name
operator: uid_op
identifier: uid
remember_roles:
authenticated: authenticated
anonymous: '0'
entity_type: user
entity_field: uid
display_plugin: default
display_title: Default
id: default
position: 0
page_1:
display_options:
path: test_user_name
display_plugin: page
display_title: Page
id: page_1
position: 0

View File

@@ -0,0 +1,114 @@
langcode: en
status: true
dependencies:
module:
- node
- user
id: test_user_relationship
label: test_user_relationship
module: views
description: ''
tag: default
base_table: node_field_data
base_field: nid
display:
default:
display_options:
access:
type: perm
cache:
type: tag
exposed_form:
type: basic
fields:
name:
alter:
absolute: false
alter_text: false
ellipsis: true
external: false
html: false
make_link: false
nl2br: false
replace_spaces: false
strip_tags: false
trim: false
trim_whitespace: false
word_boundary: true
element_default_classes: true
element_label_colon: true
empty_zero: false
field: name
hide_alter_empty: false
hide_empty: false
id: name
table: users_field_data
plugin_id: field
type: user_name
entity_type: user
entity_field: name
title:
alter:
absolute: false
alter_text: false
ellipsis: false
html: false
make_link: false
strip_tags: false
trim: false
word_boundary: false
empty_zero: false
field: title
hide_empty: false
id: title
label: ''
table: node_field_data
plugin_id: field
entity_type: node
entity_field: title
uid:
alter:
absolute: false
alter_text: false
ellipsis: true
external: false
html: false
make_link: false
nl2br: false
replace_spaces: false
strip_tags: false
trim: false
trim_whitespace: false
word_boundary: true
element_default_classes: true
element_label_colon: true
empty_zero: false
field: uid
hide_alter_empty: false
hide_empty: false
id: uid
table: users_field_data
plugin_id: field
type: user
entity_type: user
entity_field: uid
pager:
options:
items_per_page: 10
type: full
query:
options:
query_comment: ''
type: views_query
title: test_user_relationship
style:
type: default
row:
type: fields
options:
default_field_elements: true
hide_empty: false
display_plugin: default
display_title: Default
id: default
position: 0

View File

@@ -0,0 +1,218 @@
langcode: en
status: true
dependencies:
module:
- user
id: test_user_roles_rid
label: test_user_roles_rid
module: views
description: ''
tag: ''
base_table: users_field_data
base_field: uid
display:
default:
display_plugin: default
id: default
display_title: Default
position: 0
display_options:
access:
type: none
options: { }
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: full
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous: ' Previous'
next: 'Next '
first: '« First'
last: 'Last »'
quantity: 9
style:
type: default
options:
grouping: { }
row_class: ''
default_row_class: true
uses_fields: false
row:
type: fields
options:
inline: { }
separator: ''
hide_empty: false
default_field_elements: true
fields:
name:
id: name
table: users_field_data
field: name
entity_type: user
entity_field: name
label: ''
alter:
alter_text: false
make_link: false
absolute: false
trim: false
word_boundary: false
ellipsis: false
strip_tags: false
html: false
hide_empty: false
empty_zero: false
plugin_id: field
relationship: none
group_type: group
admin_label: ''
exclude: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_alter_empty: true
click_sort_column: value
type: user_name
settings: { }
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
filters:
status:
value: '1'
table: users_field_data
field: status
plugin_id: boolean
entity_type: user
entity_field: status
id: status
expose:
operator: ''
group: 1
sorts:
uid:
id: uid
table: users
field: uid
relationship: none
group_type: group
admin_label: ''
order: ASC
exposed: false
expose:
label: ''
entity_type: user
entity_field: uid
plugin_id: standard
header: { }
footer: { }
empty: { }
relationships: { }
arguments:
roles_target_id:
id: roles_target_id
table: user__roles
field: roles_target_id
relationship: none
group_type: group
admin_label: ''
default_action: empty
exception:
value: all
title_enable: false
title: All
title_enable: true
title: '{{ arguments.roles_target_id }}'
default_argument_type: fixed
default_argument_options:
argument: ''
summary_options:
base_path: ''
count: true
items_per_page: 25
override: false
summary:
sort_order: asc
number_of_records: 0
format: default_summary
specify_validation: false
validate:
type: none
fail: 'not found'
validate_options: { }
break_phrase: false
add_table: false
require_value: false
reduce_duplicates: false
plugin_id: user__roles_rid
display_extenders: { }
cache_metadata:
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- url.query_args
- user.permissions
cacheable: false
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
display_extenders: { }
path: user_roles_rid_test
cache_metadata:
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- url.query_args
- user.permissions
cacheable: false

View File

@@ -0,0 +1,37 @@
langcode: en
status: true
dependencies:
module:
- user
id: test_user_uid_argument
label: test_user_uid_argument
module: views
description: ''
tag: ''
base_table: users_field_data
base_field: uid
display:
default:
display_options:
fields:
uid:
id: uid
table: users_field_data
field: uid
plugin_id: user
entity_type: user
entity_field: uid
arguments:
uid:
id: uid
table: users_field_data
field: uid
title_enable: true
title: '{{ arguments.uid }}'
plugin_id: user_uid
entity_type: user
entity_field: uid
display_plugin: default
display_title: Default
id: default
position: 0

View File

@@ -0,0 +1,39 @@
langcode: en
status: true
dependencies: { }
id: test_view_argument_validate_user
label: test_view_argument_validate_user
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
display:
default:
display_options:
access:
type: none
arguments:
'null':
default_argument_type: fixed
field: 'null'
id: 'null'
must_not_be: false
table: views
validate:
type: 'entity:user'
plugin_id: 'null'
cache:
type: tag
exposed_form:
type: basic
pager:
type: full
style:
type: default
row:
type: fields
display_plugin: default
display_title: Default
id: default
position: 0

View File

@@ -0,0 +1,39 @@
langcode: en
status: true
dependencies: { }
id: test_view_argument_validate_username
label: test_view_argument_validate_username
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
display:
default:
display_options:
access:
type: none
arguments:
'null':
default_argument_type: fixed
field: 'null'
id: 'null'
must_not_be: false
table: views
validate:
type: user_name
plugin_id: 'null'
cache:
type: tag
exposed_form:
type: basic
pager:
type: full
style:
type: default
row:
type: fields
display_plugin: default
display_title: Default
id: default
position: 0

View File

@@ -0,0 +1,176 @@
langcode: en
status: true
dependencies:
module:
- user
id: test_views_handler_field_role
label: test_views_handler_field_role
module: views
description: ''
tag: ''
base_table: users_field_data
base_field: uid
display:
default:
display_plugin: default
id: default
display_title: Default
position: null
display_options:
access:
type: perm
options:
perm: 'access user profiles'
cache:
type: tag
query:
type: views_query
exposed_form:
type: basic
pager:
type: none
options:
items_per_page: null
style:
type: default
row:
type: fields
fields:
name:
id: name
table: users_field_data
field: name
relationship: none
group_type: group
admin_label: ''
label: Name
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
plugin_id: field
type: user_name
entity_type: user
entity_field: name
roles_target_id:
id: roles_target_id
table: user__roles
field: roles_target_id
relationship: none
group_type: group
admin_label: ''
label: Roles
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
type: separator
separator: ''
plugin_id: user_roles
filters:
status:
value: '1'
table: users_field_data
field: status
id: status
expose:
operator: '0'
group: 1
plugin_id: boolean
entity_type: user
entity_field: status
sorts:
uid:
id: uid
table: users_field_data
field: uid
relationship: none
group_type: group
admin_label: ''
order: ASC
exposed: false
expose:
label: ''
entity_type: user
entity_field: uid
plugin_id: standard
title: test_user_role
page_1:
display_plugin: page
id: page_1
display_title: Page
position: null
display_options:
path: test-views-handler-field-role

View File

@@ -0,0 +1,62 @@
langcode: en
status: true
dependencies:
module:
- user
id: test_views_handler_field_user_name
label: test_views_handler_field_user_name
module: views
description: ''
tag: default
base_table: users_field_data
base_field: nid
display:
default:
display_options:
access:
type: none
cache:
type: tag
exposed_form:
type: basic
fields:
name:
alter:
absolute: false
alter_text: false
ellipsis: false
html: false
make_link: false
strip_tags: false
trim: false
word_boundary: false
empty_zero: false
field: name
hide_empty: false
id: name
label: ''
table: users_field_data
plugin_id: field
type: user_name
entity_type: user
entity_field: name
pager:
type: full
query:
options:
query_comment: ''
type: views_query
style:
type: default
row:
type: fields
sorts:
uid:
id: uid
table: users
field: uid
plugin_id: standard
display_plugin: default
display_title: Default
id: default
position: 0

View File

@@ -0,0 +1,13 @@
name: 'User test views'
type: module
description: 'Provides default views for views user tests.'
package: Testing
# version: VERSION
dependencies:
- drupal:user
- drupal:views
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

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);
}
}

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