diff --git a/core/modules/system/tests/modules/test_page_test/src/Form/TestForm.php b/core/modules/system/tests/modules/test_page_test/src/Form/TestForm.php index 24a9bdb58b..98ddc7cb8b 100644 --- a/core/modules/system/tests/modules/test_page_test/src/Form/TestForm.php +++ b/core/modules/system/tests/modules/test_page_test/src/Form/TestForm.php @@ -64,6 +64,18 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#default_value' => 2, ]; + $form['duplicate_button'] = [ + '#type' => 'submit', + '#name' => 'duplicate_button', + '#value' => 'Duplicate button 1', + ]; + + $form['duplicate_button_2'] = [ + '#type' => 'submit', + '#name' => 'duplicate_button', + '#value' => 'Duplicate button 2', + ]; + $form['save'] = [ '#type' => 'submit', '#value' => $this->t('Save'), diff --git a/core/modules/user/tests/src/Functional/UserAdminLanguageTest.php b/core/modules/user/tests/src/Functional/UserAdminLanguageTest.php new file mode 100644 index 0000000000..414f159770 --- /dev/null +++ b/core/modules/user/tests/src/Functional/UserAdminLanguageTest.php @@ -0,0 +1,184 @@ +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() { + $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->assertNull($this->getSession()->getPage()->findField('edit-preferred-admin-langcode')); + } + + /** + * Tests that admin language negotiation is configurable only if enabled. + */ + public function testUserAdminLanguageConfigurationAvailableWithAdminLanguageNegotiation() { + $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->assertNull($this->getSession()->getPage()->findField('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->assertNotNull($this->getSession()->getPage()->findField('edit-preferred-admin-langcode')); + } + + /** + * Tests that the admin language is configurable only for administrators. + * + * If a user has the permission "access administration pages", 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() { + $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->assertNotNull($this->getSession()->getPage()->findField( '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->assertNull($this->getSession()->getPage()->findField('edit-preferred-admin-langcode')); + } + + /** + * Tests the actual language negotiation. + */ + public function testActualNegotiation() { + $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->assertText('Language negotiation method: language-default'); + $this->drupalGet('xx/' . $path); + $this->assertText('Language negotiation method: language-url'); + + // Set a preferred language code for the user. + $edit = []; + $edit['preferred_admin_langcode'] = 'xx'; + $this->drupalPostForm($path, $edit, t('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->assertText('Language negotiation method: language-user-admin'); + $this->drupalGet('xx/' . $path); + $this->assertText('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->assertText('Language negotiation method: language-user-admin'); + $this->drupalGet('xx/' . $path); + $this->assertText('Language negotiation method: language-user-admin'); + + // Unset the preferred language code for the user. + $edit = []; + $edit['preferred_admin_langcode'] = ''; + $this->drupalPostForm($path, $edit, t('Save')); + $this->drupalGet($path); + $this->assertText('Language negotiation method: language-default'); + $this->drupalGet('xx/' . $path); + $this->assertText('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->drupalPostForm('admin/config/regional/language/detection', $edit, t('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->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language')); + } + +} diff --git a/core/modules/user/tests/src/Functional/UserBlocksTest.php b/core/modules/user/tests/src/Functional/UserBlocksTest.php new file mode 100644 index 0000000000..6c2ae97609 --- /dev/null +++ b/core/modules/user/tests/src/Functional/UserBlocksTest.php @@ -0,0 +1,124 @@ +adminUser = $this->drupalCreateUser(['administer blocks']); + $this->drupalLogin($this->adminUser); + $this->drupalPlaceBlock('user_login_block'); + $this->drupalLogout($this->adminUser); + } + + /** + * Tests that user login block is hidden from user/login. + */ + public function testUserLoginBlockVisibility() { + // 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); + $elements = $this->xpath('//div[contains(@class,"block-user-login-block") and @role="form"]'); + if ($expected_visibility) { + $this->assertTrue(!empty($elements), 'User login block in path "' . $path . '" should be visible'); + } + else { + $this->assertTrue(empty($elements), 'User login block in path "' . $path . '" should not be visible'); + } + } + } + + /** + * Test the user login block. + */ + public function testUserLoginBlock() { + // Create a user with some permission that anonymous users lack. + $user = $this->drupalCreateUser(['administer permissions']); + + // Log in using the block. + $edit = []; + $edit['name'] = $user->getUsername(); + $edit['pass'] = $user->passRaw; + $this->drupalPostForm('admin/people/permissions', $edit, t('Log in')); + $this->assertNoText(t('User login'), 'Logged in.'); + + // Check that we are still on the same page. + $this->assertUrl(\Drupal::url('user.admin_permissions', [], ['absolute' => TRUE]), [], 'Still on the same page after login for access denied page'); + + // Now, log out and repeat with a non-403 page. + $this->drupalLogout(); + $this->drupalGet('filter/tips'); + $this->assertEqual('MISS', $this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER)); + $this->drupalPostForm(NULL, $edit, t('Log in')); + $this->assertNoText(t('User login'), 'Logged in.'); + $this->assertPattern('!!', 'Still on the same page after login for allowed page'); + + // Log out again and repeat with a non-403 page including query arguments. + $this->drupalLogout(); + $this->drupalGet('filter/tips', ['query' => ['foo' => 'bar']]); + $this->assertEqual('HIT', $this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER)); + $this->drupalPostForm(NULL, $edit, t('Log in')); + $this->assertNoText(t('User login'), 'Logged in.'); + $this->assertPattern('!!', 'Still on the same page after login for allowed page'); + $this->assertTrue(strpos($this->getUrl(), '/filter/tips?foo=bar') !== FALSE, 'Correct query arguments are displayed after login'); + + // Repeat with different query arguments. + $this->drupalLogout(); + $this->drupalGet('filter/tips', ['query' => ['foo' => 'baz']]); + $this->assertEqual('HIT', $this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER)); + $this->drupalPostForm(NULL, $edit, t('Log in')); + $this->assertNoText(t('User login'), 'Logged in.'); + $this->assertPattern('!!', 'Still on the same page after login for allowed page'); + $this->assertTrue(strpos($this->getUrl(), '/filter/tips?foo=baz') !== FALSE, '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->drupalPostForm('http://example.com/', $edit, t('Log in'), ['external' => FALSE]); + // Check that we remain on the site after login. + $this->assertUrl($user->url('canonical', ['absolute' => TRUE]), [], 'Redirected to user profile page after login from the frontpage'); + + // 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->drupalPostForm('filter/tips', $edit, t('Log in')); + $this->assertText(t('Unrecognized username or password. Forgot your password?')); + $this->drupalGet('filter/tips'); + $this->assertNoText(t('Unrecognized username or password. Forgot your password?')); + } + +} diff --git a/core/modules/user/tests/src/Functional/UserCreateTest.php b/core/modules/user/tests/src/Functional/UserCreateTest.php new file mode 100644 index 0000000000..fb7e996e3f --- /dev/null +++ b/core/modules/user/tests/src/Functional/UserCreateTest.php @@ -0,0 +1,134 @@ +drupalCreateUser(['administer users']); + $this->drupalLogin($user); + + $this->assertEqual($user->getCreatedTime(), REQUEST_TIME, 'Creating a user sets default "created" timestamp.'); + $this->assertEqual($user->getChangedTime(), REQUEST_TIME, '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' => t('Your virtual face or picture.'), + 'required' => FALSE, + 'settings' => [ + 'file_extensions' => 'png gif jpg jpeg', + '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->assertFieldbyId('edit-status-0', 0, 'The user status option Blocked exists.', 'User login'); + $this->assertFieldbyId('edit-status-1', 1, 'The user status option Active exists.', 'User login'); + $this->assertFieldByXPath('//input[@type="radio" and @id="edit-status-1" and @checked="checked"]', NULL, 'Default setting for user status is active.'); + + // Test that browser autocomplete behavior does not occur. + $this->assertNoRaw('data-user-info-from-browser', 'Ensure form attribute, data-user-info-from-browser, does not exist.'); + + // Test that the password strength indicator displays. + $config = $this->config('user.settings'); + + $config->set('password_strength', TRUE)->save(); + $this->drupalGet('admin/people/create'); + $this->assertRaw(t('Password strength:'), 'The password strength indicator is displayed.'); + + $config->set('password_strength', FALSE)->save(); + $this->drupalGet('admin/people/create'); + $this->assertNoRaw(t('Password strength:'), 'The password strength indicator is not displayed.'); + + // 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->drupalPostForm('admin/people/create', $edit, t('Create new account')); + + if ($notify) { + $this->assertText(t('A welcome message with further instructions has been emailed to the new user @name.', ['@name' => $edit['name']]), 'User created'); + $this->assertEqual(count($this->drupalGetMails()), 1, 'Notification email sent'); + } + else { + $this->assertText(t('Created a new user account for @name. No email has been sent.', ['@name' => $edit['name']]), 'User created'); + $this->assertEqual(count($this->drupalGetMails()), 0, 'Notification email not sent'); + } + + $this->drupalGet('admin/people'); + $this->assertText($edit['name'], 'User found in list of users'); + $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->drupalPostForm('admin/people/create', $edit, t('Create new account')); + $this->assertText("Created a new user account for $name. No email has been sent"); + $this->assertNoText('Password field is required'); + } + +} diff --git a/core/modules/user/tests/src/Functional/UserPasswordResetTest.php b/core/modules/user/tests/src/Functional/UserPasswordResetTest.php new file mode 100644 index 0000000000..fb25317075 --- /dev/null +++ b/core/modules/user/tests/src/Functional/UserPasswordResetTest.php @@ -0,0 +1,349 @@ +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 = REQUEST_TIME - mt_rand(10, 100000); + db_update('users_field_data') + ->fields(['login' => $account->getLastLoginTime()]) + ->condition('uid', $account->id()) + ->execute(); + } + + /** + * Tests password reset functionality. + */ + public function testUserPasswordReset() { + // 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->assertResponse(403); + + // Try to reset the password for an invalid account. + $this->drupalGet('user/password'); + + $edit = ['name' => $this->randomMachineName(32)]; + $this->drupalPostForm(NULL, $edit, t('Submit')); + + $this->assertText(t('@name is not recognized as a username or an email address.', ['@name' => $edit['name']]), 'Validation error message shown when trying to request password for invalid account.'); + $this->assertEqual(count($this->drupalGetMails(['id' => 'user_password_reset'])), 0, 'No email was sent when requesting a password for an invalid account.'); + + // Reset the password by username via the password reset page. + $edit['name'] = $this->account->getUsername(); + $this->drupalPostForm(NULL, $edit, t('Submit')); + + // Verify that the user was sent an email. + $this->assertMail('to', $this->account->getEmail(), 'Password email sent to user.'); + $subject = t('Replacement login information for @username at @site', ['@username' => $this->account->getUsername(), '@site' => $this->config('system.site')->get('name')]); + $this->assertMail('subject', $subject, 'Password reset email subject is correct.'); + + $resetURL = $this->getResetURL(); + $this->drupalGet($resetURL); + // Ensure that the current url does not contain the hash and timestamp. + $this->assertUrl(Url::fromRoute('user.reset.form', ['uid' => $this->account->id()])); + + $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache')); + + // Ensure the password reset URL is not cached. + $this->drupalGet($resetURL); + $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache')); + + // Check the one-time login page. + $this->assertText($this->account->getUsername(), 'One-time login page contains the correct username.'); + $this->assertText(t('This login can be used only once.'), 'Found warning about one-time login.'); + $this->assertTitle(t('Reset password | Drupal'), 'Page title is "Reset password".'); + + // Check successful login. + $this->drupalPostForm(NULL, NULL, t('Log in')); + $this->assertLink(t('Log out')); + $this->assertTitle(t('@name | @site', ['@name' => $this->account->getUsername(), '@site' => $this->config('system.site')->get('name')]), 'Logged in using password reset link.'); + + // Make sure the ajax request from uploading a user picture does not + // invalidate the reset token. + $image = current($this->drupalGetTestFiles('image')); + $edit = [ + 'files[user_picture_0]' => drupal_realpath($image->uri), + ]; + $this->drupalPostForm(NULL, $edit, 'Upload'); + + // Change the forgotten password. + $password = user_password(); + $edit = ['pass[pass1]' => $password, 'pass[pass2]' => $password]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText(t('The changes have been saved.'), 'Forgotten password changed.'); + + // Verify that the password reset session has been destroyed. + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText(t('Your current password is missing or incorrect; it\'s required to change the Password.'), 'Password needed to make profile changes.'); + + // Log out, and try to log in again using the same one-time link. + $this->drupalLogout(); + $this->drupalGet($resetURL); + $this->drupalPostForm(NULL, NULL, t('Log in')); + $this->assertText(t('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'), 'One-time link is no longer valid.'); + + // Request a new password again, this time using the email address. + $this->drupalGet('user/password'); + // Count email messages before to compare with after. + $before = count($this->drupalGetMails(['id' => 'user_password_reset'])); + $edit = ['name' => $this->account->getEmail()]; + $this->drupalPostForm(NULL, $edit, t('Submit')); + $this->assertTrue( count($this->drupalGetMails(['id' => 'user_password_reset'])) === $before + 1, '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->drupalPostForm(NULL, NULL, t('Log in')); + $this->drupalGet('user/' . $this->account->id() . '/edit'); + $this->assertNoText('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 = REQUEST_TIME - $timeout - 60; + $_uid = $this->account->id(); + $this->drupalGet("user/reset/$_uid/$bogus_timestamp/" . user_pass_rehash($this->account, $bogus_timestamp)); + $this->drupalPostForm(NULL, NULL, t('Log in')); + $this->assertText(t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.'), 'Expired password reset request rejected.'); + + // Create a user, block the account, and verify that a login link is denied. + $timestamp = REQUEST_TIME - 1; + $blocked_account = $this->drupalCreateUser()->block(); + $blocked_account->save(); + $this->drupalGet("user/reset/" . $blocked_account->id() . "/$timestamp/" . user_pass_rehash($blocked_account, $timestamp)); + $this->assertResponse(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->getUsername()]; + $this->drupalPostForm(NULL, $edit, t('Submit')); + $this->assertRaw(t('%name is blocked or has not been activated yet.', ['%name' => $blocked_account->getUsername()]), 'Notified user blocked accounts can not request a new password'); + $this->assertTrue(count($this->drupalGetMails(['id' => 'user_password_reset'])) === $before, '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->getUsername()]; + $this->drupalPostForm(NULL, $edit, t('Submit')); + $old_email_reset_link = $this->getResetURL(); + $this->account->setEmail("1" . $this->account->getEmail()); + $this->account->save(); + $this->drupalGet($old_email_reset_link); + $this->drupalPostForm(NULL, NULL, t('Log in')); + $this->assertText(t('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'), 'One-time link is no longer valid.'); + + // Verify a password reset link will automatically log a user when /login is + // appended. + $this->drupalGet('user/password'); + $edit = ['name' => $this->account->getUsername()]; + $this->drupalPostForm(NULL, $edit, t('Submit')); + $reset_url = $this->getResetURL(); + $this->drupalGet($reset_url . '/login'); + $this->assertLink(t('Log out')); + $this->assertTitle(t('@name | @site', ['@name' => $this->account->getUsername(), '@site' => $this->config('system.site')->get('name')]), 'Logged in using password reset link.'); + + // Ensure blocked and deleted accounts can't access the user.reset.login + // route. + $this->drupalLogout(); + $timestamp = REQUEST_TIME - 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->assertResponse(403); + + $blocked_account->delete(); + $this->drupalGet("user/reset/" . $blocked_account->id() . "/$timestamp/" . user_pass_rehash($blocked_account, $timestamp) . '/login'); + $this->assertResponse(403); + } + + /** + * 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]; + } + + /** + * Test user password reset while logged in. + */ + public function testUserPasswordResetLoggedIn() { + $another_account = $this->drupalCreateUser(); + $this->drupalLogin($another_account); + $this->drupalGet('user/password'); + $this->drupalPostForm(NULL, NULL, t('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->assertRaw(new FormattableMarkup( + 'Another user (%other_user) is already logged into the site on this computer, but you tried to use a one-time link for user %resetting_user. Please log out and try using the link again.', + ['%other_user' => $this->account->getUsername(), '%resetting_user' => $another_account->getUsername(), ':logout' => Url::fromRoute('user.logout')->toString()] + )); + + $another_account->delete(); + $this->drupalGet($resetURL); + $this->assertText('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->drupalPostForm(NULL, NULL, t('Submit')); + + // Click the reset URL while logged and change our password. + $resetURL = $this->getResetURL(); + $this->drupalGet($resetURL); + $this->drupalPostForm(NULL, NULL, t('Log in')); + + // Change the password. + $password = user_password(); + $edit = ['pass[pass1]' => $password, 'pass[pass2]' => $password]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText(t('The changes have been saved.'), 'Password changed.'); + + // Logged in users should not be able to access the user.reset.login or the + // user.reset.form routes. + $timestamp = REQUEST_TIME - 1; + $this->drupalGet("user/reset/" . $this->account->id() . "/$timestamp/" . user_pass_rehash($this->account, $timestamp) . '/login'); + $this->assertResponse(403); + $this->drupalGet("user/reset/" . $this->account->id()); + $this->assertResponse(403); + } + + /** + * Prefill the text box on incorrect login via link to password reset page. + */ + public function testUserResetPasswordTextboxFilled() { + $this->drupalGet('user/login'); + $edit = [ + 'name' => $this->randomMachineName(), + 'pass' => $this->randomMachineName(), + ]; + $this->drupalPostForm('user/login', $edit, t('Log in')); + $this->assertRaw(t('Unrecognized username or password. Forgot your password?', + [':password' => \Drupal::url('user.pass', [], ['query' => ['name' => $edit['name']]])])); + unset($edit['pass']); + $this->drupalGet('user/password', ['query' => ['name' => $edit['name']]]); + $this->assertFieldByName('name', $edit['name'], 'User name found.'); + // Ensure the name field value is not cached. + $this->drupalGet('user/password'); + $this->assertNoFieldByName('name', $edit['name'], 'User name not found.'); + } + + /** + * Make sure that users cannot forge password reset URLs of other users. + */ + public function testResetImpersonation() { + // 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. + db_update('users_field_data') + ->fields(['pass' => NULL]) + ->condition('uid', [$user1->id(), $user2->id()], 'IN') + ->execute(); + \Drupal::entityManager()->getStorage('user')->resetCache(); + $user1 = User::load($user1->id()); + $user2 = User::load($user2->id()); + + $this->assertEqual($user1->getPassword(), $user2->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); + $this->drupalPostForm(NULL, NULL, t('Log in')); + $this->assertNoText($user2->getUsername(), 'The invalid password reset page does not show the user name.'); + $this->assertUrl('user/password', [], 'The user is redirected to the password reset request page.'); + $this->assertText('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'); + } + +} diff --git a/core/modules/user/tests/src/Functional/UserRegistrationTest.php b/core/modules/user/tests/src/Functional/UserRegistrationTest.php new file mode 100644 index 0000000000..2ea037b1b8 --- /dev/null +++ b/core/modules/user/tests/src/Functional/UserRegistrationTest.php @@ -0,0 +1,282 @@ +config('user.settings'); + // Require email verification. + $config->set('verify_mail', TRUE)->save(); + + // Set registration to administrator only. + $config->set('register', USER_REGISTER_ADMINISTRATORS_ONLY)->save(); + $this->drupalGet('user/register'); + $this->assertResponse(403, 'Registration page is inaccessible when only administrators can create accounts.'); + + // Allow registration by site visitors without administrator approval. + $config->set('register', USER_REGISTER_VISITORS)->save(); + $edit = []; + $edit['name'] = $name = $this->randomMachineName(); + $edit['mail'] = $mail = $edit['name'] . '@example.com'; + $this->drupalPostForm('user/register', $edit, t('Create new account')); + $this->assertText(t('A welcome message with further instructions has been sent to your email address.'), 'User registered successfully.'); + + /** @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->assertTitle(t('Set password | Drupal'), 'Page title is "Set password".'); + + // Allow registration by site visitors, but require administrator approval. + $config->set('register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL)->save(); + $edit = []; + $edit['name'] = $name = $this->randomMachineName(); + $edit['mail'] = $mail = $edit['name'] . '@example.com'; + $this->drupalPostForm('user/register', $edit, t('Create new account')); + $this->container->get('entity.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() { + $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', USER_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->drupalPostForm('user/register', $edit, t('Create new account')); + $this->assertText(t('The specified passwords do not match.'), 'Typing mismatched passwords displays an error message.'); + + // Enter a correct password. + $edit['pass[pass1]'] = $new_pass = $this->randomMachineName(); + $edit['pass[pass2]'] = $new_pass; + $this->drupalPostForm('user/register', $edit, t('Create new account')); + $this->container->get('entity.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->assertText(t('Registration successful. You are now logged in.'), 'Users are logged in after registering.'); + $this->drupalLogout(); + + // Allow registration by site visitors, but require administrator approval. + $config->set('register', USER_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->drupalPostForm('user/register', $edit, t('Create new account')); + $this->assertText(t('Thank you for applying for an account. Your account is currently pending approval by the site administrator.'), 'Users are notified of pending approval'); + + // Try to log in before administrator approval. + $auth = [ + 'name' => $name, + 'pass' => $pass, + ]; + $this->drupalPostForm('user/login', $auth, t('Log in')); + $this->assertText(t('The username @name has not been activated or is blocked.', ['@name' => $name]), 'User cannot log in yet.'); + + // 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->drupalPostForm('user/' . $new_user->id() . '/edit', $edit, t('Save')); + $this->drupalLogout(); + + // Log in after administrator approval. + $this->drupalPostForm('user/login', $auth, t('Log in')); + $this->assertText(t('Member for'), 'User can log in after administrator approval.'); + } + + public function testRegistrationEmailDuplicates() { + // Don't require email verification and allow registration by site visitors + // without administrator approval. + $this->config('user.settings') + ->set('verify_mail', FALSE) + ->set('register', USER_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->drupalPostForm('user/register', $edit, t('Create new account')); + $this->assertText(t('The email address @email is already taken.', ['@email' => $duplicate_user->getEmail()]), 'Supplying an exact duplicate email address displays an error message'); + + // Attempt to bypass duplicate email registration validation by adding spaces. + $edit['mail'] = ' ' . $duplicate_user->getEmail() . ' '; + + $this->drupalPostForm('user/register', $edit, t('Create new account')); + $this->assertText(t('The email address @email is already taken.', ['@email' => $duplicate_user->getEmail()]), 'Supplying a duplicate email address with added whitespace displays an error message'); + } + + /** + * 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() { + \Drupal::service('module_installer')->install(['image']); + \Drupal::service('router.builder')->rebuild(); + + // 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', USER_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->drupalPostForm('user/register', $edit, t('Create new account')); + $this->assertResponse(200); + + $user_storage = \Drupal::entityManager()->getStorage('user'); + + $this->assertTrue($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->drupalPostForm('user/register', $edit, t('Create new account')); + $this->assertResponse(200); + + $this->assertTrue($user_storage->loadByProperties(['name' => $edit['name']])); + } + + public function testRegistrationDefaultValues() { + // 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', USER_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->assertCacheTag('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->drupalPostForm(NULL, $edit, t('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->assertEqual($new_user->getUsername(), $name, 'Username matches.'); + $this->assertEqual($new_user->getEmail(), $mail, 'Email address matches.'); + $this->assertTrue(($new_user->getCreatedTime() > REQUEST_TIME - 20), 'Correct creation time.'); + $this->assertEqual($new_user->isActive(), $config_user_settings->get('register') == USER_REGISTER_VISITORS ? 1 : 0, 'Correct status field.'); + $this->assertEqual($new_user->getTimezone(), $config_system_date->get('timezone.default'), 'Correct time zone field.'); + $this->assertEqual($new_user->langcode->value, \Drupal::languageManager()->getDefaultLanguage()->getId(), 'Correct language field.'); + $this->assertEqual($new_user->preferred_langcode->value, \Drupal::languageManager()->getDefaultLanguage()->getId(), 'Correct preferred language field.'); + $this->assertEqual($new_user->init->value, $mail, '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() { + $account = $this->drupalCreateUser(); + + $edit = ['mail' => 'test@example.com', 'name' => $account->getUsername()]; + $this->drupalPostForm('user/register', $edit, t('Create new account')); + $this->assertRaw(SafeMarkup::format('The username %value is already taken.', ['%value' => $account->getUsername()])); + + $edit = ['mail' => $account->getEmail(), 'name' => $this->randomString()]; + $this->drupalPostForm('user/register', $edit, t('Create new account')); + $this->assertRaw(SafeMarkup::format('The email address %value is already taken.', ['%value' => $account->getEmail()])); + } + +} diff --git a/core/tests/Drupal/FunctionalTests/AssertLegacyTrait.php b/core/tests/Drupal/FunctionalTests/AssertLegacyTrait.php index 08a2d8ea19..f768b71ad5 100644 --- a/core/tests/Drupal/FunctionalTests/AssertLegacyTrait.php +++ b/core/tests/Drupal/FunctionalTests/AssertLegacyTrait.php @@ -2,7 +2,6 @@ namespace Drupal\FunctionalTests; -use Behat\Mink\Exception\ElementNotFoundException; use Behat\Mink\Exception\ExpectationException; use Behat\Mink\Selector\Xpath\Escaper; use Drupal\Component\Render\FormattableMarkup; @@ -220,13 +219,11 @@ protected function assertResponse($code) { * * @deprecated Scheduled for removal in Drupal 9.0.0. * Use $this->assertSession()->fieldExists() or + * $this->assertSession()->buttonExists() or * $this->assertSession()->fieldValueEquals() instead. */ protected function assertFieldByName($name, $value = NULL) { - $this->assertSession()->fieldExists($name); - if ($value !== NULL) { - $this->assertSession()->fieldValueEquals($name, (string) $value); - } + $this->assertFieldByXPath($this->constructFieldXpath('name', $name), $value); } /** @@ -242,15 +239,11 @@ protected function assertFieldByName($name, $value = NULL) { * * @deprecated Scheduled for removal in Drupal 9.0.0. * Use $this->assertSession()->fieldNotExists() or + * $this->assertSession()->buttonNotExists() or * $this->assertSession()->fieldValueNotEquals() instead. */ protected function assertNoFieldByName($name, $value = '') { - if ($this->getSession()->getPage()->findField($name) && isset($value)) { - $this->assertSession()->fieldValueNotEquals($name, (string) $value); - } - else { - $this->assertSession()->fieldNotExists($name); - } + $this->assertNoFieldByXPath($this->constructFieldXpath('name', $name), $value); } /** @@ -268,19 +261,11 @@ protected function assertNoFieldByName($name, $value = '') { * * @deprecated Scheduled for removal in Drupal 9.0.0. * Use $this->assertSession()->fieldExists() or + * $this->assertSession()->buttonExists() or * $this->assertSession()->fieldValueEquals() instead. */ protected function assertFieldById($id, $value = '') { - $xpath = $this->assertSession()->buildXPathQuery('//textarea[@id=:value]|//input[@id=:value]|//select[@id=:value]', [':value' => $id]); - $field = $this->getSession()->getPage()->find('xpath', $xpath); - - if (empty($field)) { - throw new ElementNotFoundException($this->getSession()->getDriver(), 'form field', 'id', $field); - } - - if ($value !== NULL) { - $this->assertEquals($value, $field->getValue()); - } + $this->assertFieldByXPath($this->constructFieldXpath('id', $id), $value); } /** @@ -290,23 +275,25 @@ protected function assertFieldById($id, $value = '') { * Name or ID of field to assert. * * @deprecated Scheduled for removal in Drupal 9.0.0. - * Use $this->assertSession()->fieldExists() instead. + * Use $this->assertSession()->fieldExists() or + * $this->assertSession()->buttonExists() instead. */ protected function assertField($field) { - $this->assertSession()->fieldExists($field); + $this->assertFieldByXPath($this->constructFieldXpath('name', $field) . '|' . $this->constructFieldXpath('id', $field)); } /** - * Asserts that a field exists with the given name or ID does NOT exist. + * Asserts that a field does NOT exist with the given name or ID. * * @param string $field * Name or ID of field to assert. * * @deprecated Scheduled for removal in Drupal 9.0.0. - * Use $this->assertSession()->fieldNotExists() instead. + * Use $this->assertSession()->fieldNotExists() or + * $this->assertSession()->buttonNotExists() instead. */ protected function assertNoField($field) { - $this->assertSession()->fieldNotExists($field); + $this->assertNoFieldByXPath($this->constructFieldXpath('name', $field) . '|' . $this->constructFieldXpath('id', $field)); } /** @@ -427,23 +414,11 @@ protected function assertNoLinkByHref($href) { * * @deprecated Scheduled for removal in Drupal 9.0.0. * Use $this->assertSession()->fieldNotExists() or + * $this->assertSession()->buttonNotExists() or * $this->assertSession()->fieldValueNotEquals() instead. */ protected function assertNoFieldById($id, $value = '') { - $xpath = $this->assertSession()->buildXPathQuery('//textarea[@id=:value]|//input[@id=:value]|//select[@id=:value]', [':value' => $id]); - $field = $this->getSession()->getPage()->find('xpath', $xpath); - - // Return early if the field could not be found as expected. - if ($field === NULL) { - return; - } - - if (!isset($value)) { - throw new ExpectationException(sprintf('Id "%s" appears on this page, but it should not.', $id), $this->getSession()->getDriver()); - } - elseif ($value === $field->getValue()) { - throw new ExpectationException(sprintf('Failed asserting that %s is not equal to %s', $field->getValue(), $value), $this->getSession()->getDriver()); - } + $this->assertNoFieldByXPath($this->constructFieldXpath('id', $id), $value); } /** @@ -585,25 +560,32 @@ protected function assertFieldByXPath($xpath, $value = NULL, $message = '') { * (optional) A message to display with the assertion. Do not translate * messages with t(). * + * @throws \Behat\Mink\Exception\ExpectationException + * * @deprecated Scheduled for removal in Drupal 9.0.0. * Use $this->xpath() instead and assert that the result is empty. */ protected function assertNoFieldByXPath($xpath, $value = NULL, $message = '') { $fields = $this->xpath($xpath); - // If value specified then check array for match. - $found = TRUE; - if (isset($value)) { - $found = FALSE; - if ($fields) { - foreach ($fields as $field) { - if ($field->getAttribute('value') == $value) { - $found = TRUE; - } + if (!empty($fields)) { + if (isset($value)) { + $found = FALSE; + try { + $this->assertFieldsByValue($fields, $value); + $found = TRUE; + } + catch (\Exception $e) { + } + + if ($found) { + throw new ExpectationException(sprintf('The field resulting from %s was found with the provided value %s.', $xpath, $value), $this->getSession()->getDriver()); } } + else { + throw new ExpectationException(sprintf('The field resulting from %s was found.', $xpath), $this->getSession()->getDriver()); + } } - return $this->assertFalse($fields && $found, $message); } /** @@ -629,7 +611,15 @@ protected function assertFieldsByValue($fields, $value = NULL, $message = '') { $found = FALSE; if ($fields) { foreach ($fields as $field) { - if ($field->getAttribute('value') == $value) { + if ($field->getAttribute('type') == 'checkbox') { + if (is_bool($value)) { + $found = $field->isChecked() == $value; + } + else { + $found = TRUE; + } + } + elseif ($field->getAttribute('value') == $value) { // Input element with correct value. $found = TRUE; } @@ -637,7 +627,7 @@ protected function assertFieldsByValue($fields, $value = NULL, $message = '') { // Select element with an option. $found = TRUE; } - elseif ($field->getText() == $value) { + elseif ($field->getTagName() !== 'input' && $field->getText() == $value) { // Text area with correct text. $found = TRUE; } @@ -789,6 +779,25 @@ protected function buildXPathQuery($xpath, array $args = []) { } /** + * Helper: Constructs an XPath for the given set of attributes and value. + * + * @param string $attribute + * Field attributes. + * @param string $value + * Value of field. + * + * @return string + * XPath for specified values. + * + * @deprecated Scheduled for removal in Drupal 9.0.0. + * Use $this->getSession()->getPage()->findField() instead. + */ + protected function constructFieldXpath($attribute, $value) { + $xpath = '//textarea[@' . $attribute . '=:value]|//input[@' . $attribute . '=:value]|//select[@' . $attribute . '=:value]'; + return $this->buildXPathQuery($xpath, [':value' => $value]); + } + + /** * Gets the current raw content. * * @deprecated Scheduled for removal in Drupal 9.0.0. diff --git a/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php b/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php index 3a0750113c..4a831d8764 100644 --- a/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php +++ b/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php @@ -181,7 +181,7 @@ public function testLegacyXpathAsserts() { $this->assertNoFieldByXPath("//input[@id = 'edit-name']"); $this->fail('The "edit-name" field was not found.'); } - catch (\PHPUnit_Framework_ExpectationFailedException $e) { + catch (ExpectationException $e) { $this->pass('assertNoFieldByXPath correctly failed. The "edit-name" field was found.'); } @@ -197,7 +197,7 @@ public function testLegacyXpathAsserts() { /** * Tests legacy field asserts using textfields. */ - public function testLegacyFieldAssertsWithTextfields() { + public function testLegacyFieldAssertsForTextfields() { $this->drupalGet('test-field-xpath'); // *** 1. assertNoField(). @@ -230,7 +230,7 @@ public function testLegacyFieldAssertsWithTextfields() { $this->assertField('invalid_name_and_id'); $this->fail('The "invalid_name_and_id" field was found.'); } - catch (ExpectationException $e) { + catch (\PHPUnit_Framework_ExpectationFailedException $e) { $this->pass('assertField correctly failed. The "invalid_name_and_id" field was not found.'); } @@ -319,7 +319,7 @@ public function testLegacyFieldAssertsWithTextfields() { $this->assertFieldByName('non-existing-name'); $this->fail('The "non-existing-name" field was found.'); } - catch (ExpectationException $e) { + catch (\PHPUnit_Framework_ExpectationFailedException $e) { $this->pass('The "non-existing-name" field was not found'); } @@ -328,15 +328,15 @@ public function testLegacyFieldAssertsWithTextfields() { $this->assertFieldByName('name', 'not the value'); $this->fail('The "name" field with incorrect value was found.'); } - catch (ExpectationException $e) { + catch (\PHPUnit_Framework_ExpectationFailedException $e) { $this->pass('assertFieldByName correctly failed. The "name" field with incorrect value was not found.'); } } /** - * Tests legacy field asserts on other types of field. + * Tests legacy field asserts for options field type. */ - public function testLegacyFieldAssertsWithNonTextfields() { + public function testLegacyFieldAssertsForOptions() { $this->drupalGet('test-field-xpath'); // Option field type. @@ -383,8 +383,14 @@ public function testLegacyFieldAssertsWithNonTextfields() { catch (\PHPUnit_Framework_ExpectationFailedException $e) { $this->pass($e->getMessage()); } + } + + /** + * Tests legacy field asserts for button field type. + */ + public function testLegacyFieldAssertsForButton() { + $this->drupalGet('test-field-xpath'); - // Button field type. $this->assertFieldById('edit-save', NULL); // Test that the assertion fails correctly if the field value is passed in // rather than the id. @@ -392,7 +398,7 @@ public function testLegacyFieldAssertsWithNonTextfields() { $this->assertFieldById('Save', NULL); $this->fail('The field with id of "Save" was found.'); } - catch (ExpectationException $e) { + catch (\PHPUnit_Framework_ExpectationFailedException $e) { $this->pass($e->getMessage()); } @@ -407,7 +413,27 @@ public function testLegacyFieldAssertsWithNonTextfields() { $this->pass($e->getMessage()); } - // Checkbox field type. + // Test that multiple fields with the same name are validated correctly. + $this->assertFieldByName('duplicate_button', 'Duplicate button 1'); + $this->assertFieldByName('duplicate_button', 'Duplicate button 2'); + $this->assertNoFieldByName('duplicate_button', 'Rabbit'); + + try { + $this->assertNoFieldByName('duplicate_button', 'Duplicate button 2'); + $this->fail('The "duplicate_button" field with the value Duplicate button 2 was not found.'); + } + catch (ExpectationException $e) { + $this->pass('assertNoFieldByName correctly failed. The "duplicate_button" field with the value Duplicate button 2 was found.'); + } + } + + /** + * Tests legacy field asserts for checkbox field type. + */ + public function testLegacyFieldAssertsForCheckbox() { + $this->drupalGet('test-field-xpath'); + + // Part 1 - Test by name. // Test that checkboxes are found/not found correctly by name, when using // TRUE or FALSE to match their 'checked' state. $this->assertFieldByName('checkbox_enabled', TRUE); @@ -420,6 +446,24 @@ public function testLegacyFieldAssertsWithNonTextfields() { $this->assertFieldByName('checkbox_enabled', NULL); $this->assertFieldByName('checkbox_disabled', NULL); + // Test that checkboxes are found by name when passing no second parameter. + $this->assertFieldByName('checkbox_enabled'); + $this->assertFieldByName('checkbox_disabled'); + + // Test that we have legacy support. + $this->assertFieldByName('checkbox_enabled', '1'); + $this->assertFieldByName('checkbox_disabled', ''); + + // Test that the assertion fails correctly when using NULL to ignore state. + try { + $this->assertNoFieldByName('checkbox_enabled', NULL); + $this->fail('The "checkbox_enabled" field was not found by name, using NULL value.'); + } + catch (ExpectationException $e) { + $this->pass('assertNoFieldByName failed correctly. The "checkbox_enabled" field was found using NULL value.'); + } + + // Part 2 - Test by ID. // Test that checkboxes are found/not found correctly by ID, when using // TRUE or FALSE to match their 'checked' state. $this->assertFieldById('edit-checkbox-enabled', TRUE); @@ -427,19 +471,18 @@ public function testLegacyFieldAssertsWithNonTextfields() { $this->assertNoFieldById('edit-checkbox-enabled', FALSE); $this->assertNoFieldById('edit-checkbox-disabled', TRUE); - // Test that checkboxes are found by by ID, when using NULL to ignore the + // Test that checkboxes are found by ID, when using NULL to ignore the // 'checked' state. $this->assertFieldById('edit-checkbox-enabled', NULL); $this->assertFieldById('edit-checkbox-disabled', NULL); - // Test that the assertion fails correctly when using NULL to ignore state. - try { - $this->assertNoFieldByName('checkbox_enabled', NULL); - $this->fail('The "checkbox_enabled" field was not found by name, using NULL value.'); - } - catch (ExpectationException $e) { - $this->pass('assertNoFieldByName failed correctly. The "checkbox_enabled" field was found using NULL value.'); - } + // Test that checkboxes are found by ID when passing no second parameter. + $this->assertFieldById('edit-checkbox-enabled'); + $this->assertFieldById('edit-checkbox-disabled'); + + // Test that we have legacy support. + $this->assertFieldById('edit-checkbox-enabled', '1'); + $this->assertFieldById('edit-checkbox-disabled', ''); // Test that the assertion fails correctly when using NULL to ignore state. try { @@ -450,7 +493,7 @@ public function testLegacyFieldAssertsWithNonTextfields() { $this->pass('assertNoFieldById failed correctly. The "edit-checkbox-disabled" field was found by ID using NULL value.'); } - // Test the specific 'checked' assertions. + // Part 3 - Test the specific 'checked' assertions. $this->assertFieldChecked('edit-checkbox-enabled'); $this->assertNoFieldChecked('edit-checkbox-disabled');