URL: https://github.com/freeipa/freeipa/pull/1838
Author: Rezney
 Title: #1838: ui_tests: extend test_user suite
Action: opened

PR body:
"""
ui_tests: extend test_user suite
    
    Extend WebUI test_user suite with the following test cases:
    
    test_add_user_special
    test_user_misc
    test_ssh_keys
    test_add_delete_undo_reset
    test_disable_delete_admin
    test_login_without_username
    
https://pagure.io/freeipa/issue/7507
"""

To pull the PR as Git branch:
git remote add ghfreeipa https://github.com/freeipa/freeipa
git fetch ghfreeipa pull/1838/head:pr1838
git checkout pr1838
From 83d11b8233db91a3d61555f4a0c111378e1d1ba4 Mon Sep 17 00:00:00 2001
From: Michal Reznik <mrez...@redhat.com>
Date: Thu, 19 Apr 2018 15:19:37 +0200
Subject: [PATCH 1/3] ui_tests: extend test_user suite

Extend WebUI test_user suite with the following test cases:

test_add_user_special
test_user_misc
test_ssh_keys
test_add_delete_undo_reset
test_disable_delete_admin
test_login_without_username

https://pagure.io/freeipa/issue/7507
---
 ipatests/test_webui/data_user.py | 155 ++++++++++++++
 ipatests/test_webui/test_user.py | 443 ++++++++++++++++++++++++++++++++++++++-
 2 files changed, 593 insertions(+), 5 deletions(-)

diff --git a/ipatests/test_webui/data_user.py b/ipatests/test_webui/data_user.py
index c5ed796c7b..87048bba5b 100644
--- a/ipatests/test_webui/data_user.py
+++ b/ipatests/test_webui/data_user.py
@@ -17,6 +17,7 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+
 ENTITY = 'user'
 
 PKEY = 'itest-user'
@@ -35,17 +36,67 @@
     'mod': [
         ('textbox', 'givenname', 'OtherName'),
         ('textbox', 'sn', 'OtherSurname'),
+        ('textbox', 'initials', 'NOS'),
+        ('textbox', 'loginshell', '/bin/csh'),
+        ('textbox', 'homedirectory', '/home/alias'),
         ('multivalued', 'telephonenumber', [
             ('add', '123456789'),
             ('add', '987654321'),
         ]),
+        ('multivalued', 'mail', [
+            ('add', 'o...@ipa.test'),
+            ('add', 't...@ipa.test'),
+            ('add', 'th...@ipa.test'),
+        ]),
+        ('multivalued', 'pager', [
+            ('add', '1234567'),
+            ('add', '7654321'),
+        ]),
+        ('multivalued', 'mobile', [
+            ('add', '001123456'),
+            ('add', '001654321'),
+        ]),
+        ('multivalued', 'facsimiletelephonenumber', [
+            ('add', '1122334'),
+            ('add', '4332211'),
+        ]),
+        ('textbox', 'street', 'Wonderwall ave.'),
+        ('textbox', 'l', 'Atlantis'),
+        ('textbox', 'st', 'Universe'),
+        ('textbox', 'postalcode', '61600'),
+        ('multivalued', 'carlicense', [
+            ('add', 'ZLA-1336'),
+        ]),
+        ('textbox', 'ou', 'QE'),
         ('combobox', 'manager', 'admin'),
+        ('textbox', 'employeenumber', '123'),
+        ('textbox', 'employeetype', 'contractor'),
+        ('textbox', 'preferredlanguage', 'Spanish'),
     ],
     'mod_v': [
         ('textbox', 'givenname', 'OtherName'),
         ('textbox', 'sn', 'OtherSurname'),
+        ('textbox', 'initials', 'NOS'),
+        ('textbox', 'loginshell', '/bin/csh'),
+        ('textbox', 'homedirectory', '/home/alias'),
+        ('label', 'krbmaxrenewableage', '604800'),
+        ('label', 'krbmaxticketlife', '86400'),
         ('multivalued', 'telephonenumber', ['123456789', '987654321']),
+        ('multivalued', 'mail', ['o...@ipa.test', 't...@ipa.test',
+                                 'th...@ipa.test']),
+        ('multivalued', 'pager', ['1234567', '7654321']),
+        ('multivalued', 'mobile', ['001123456', '001654321']),
+        ('multivalued', 'facsimiletelephonenumber', ['1122334', '4332211']),
+        ('textbox', 'street', 'Wonderwall ave.'),
+        ('textbox', 'l', 'Atlantis'),
+        ('textbox', 'st', 'Universe'),
+        ('textbox', 'postalcode', '61600'),
+        ('multivalued', 'carlicense', ['ZLA-1336']),
+        ('textbox', 'ou', 'QE'),
         ('combobox', 'manager', 'admin'),
+        ('textbox', 'employeenumber', '123'),
+        ('textbox', 'employeetype', 'contractor'),
+        ('textbox', 'preferredlanguage', 'Spanish'),
     ],
 }
 
@@ -71,6 +122,8 @@
         ('textbox', 'givenname', 'Name3'),
         ('textbox', 'sn', 'Surname3'),
         ('checkbox', 'noprivate', None),
+        ('password', 'userpassword', 'Supersecret123'),
+        ('password', 'userpassword2', 'Supersecret123'),
     ]
 }
 
@@ -85,3 +138,105 @@
         ('combobox', 'gidnumber', '77777'),
     ]
 }
+
+PKEY5 = '1spe.cial_us-er$'
+DATA5 = {
+    'pkey': PKEY5,
+    'add': [
+        ('textbox', 'uid', PKEY5),
+        ('textbox', 'givenname', 'S$p|e>c--i_a%l_'),
+        ('textbox', 'sn', '%U&s?e+r'),
+        ('password', 'userpassword', '!!!@@@###$$$'),
+        ('password', 'userpassword2', '!!!@@@###$$$'),
+    ]
+}
+
+PKEY6 = 'itest-user' * 5
+DATA6 = {
+    'pkey': PKEY6,
+    'add': [
+        ('textbox', 'uid', PKEY6),
+        ('textbox', 'givenname', 'Name8'),
+        ('textbox', 'sn', 'Surname8'),
+    ]
+}
+
+PKEY7 = 'itest-user-passwd-leading-space'
+DATA7 = {
+    'pkey': PKEY7,
+    'add': [
+        ('textbox', 'uid', PKEY7),
+        ('textbox', 'givenname', 'Name7'),
+        ('textbox', 'sn', 'Surname7'),
+        ('password', 'userpassword', ' Password123 '),
+        ('password', 'userpassword2', ' Password123 '),
+    ]
+}
+
+PKEY8 = 'itest-user-passwd-trailing-space'
+DATA8 = {
+    'pkey': PKEY8,
+    'add': [
+        ('textbox', 'uid', PKEY8),
+        ('textbox', 'givenname', 'Name8'),
+        ('textbox', 'sn', 'Surname8'),
+        ('password', 'userpassword', 'Password123 '),
+        ('password', 'userpassword2', 'Password123 '),
+    ]
+}
+
+PKEY9 = 'itest-user-passwd-mismatch'
+DATA9 = {
+    'pkey': PKEY9,
+    'add': [
+        ('textbox', 'uid', PKEY9),
+        ('textbox', 'givenname', 'Name9'),
+        ('textbox', 'sn', 'Surname9'),
+        ('password', 'userpassword', 'Password123'),
+        ('password', 'userpassword2', 'Password12'),
+    ]
+}
+
+PKEY10 = 'itest-user-no-login'
+DATA10 = {
+    'pkey': PKEY10,
+    'add': [
+        ('textbox', 'givenname', 'Name10'),
+        ('textbox', 'sn', 'Surname10'),
+        ('password', 'userpassword', 'Password123'),
+        ('password', 'userpassword2', 'Password123'),
+    ]
+}
+
+SSH_RSA = (
+    'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBVmLXpTDhrYkABOPlADFk'
+    'GV8/QfgQqUQ0xn29hk18t/NTEQOW/Daq4EF84e9aTiopRXIk7jahBLzwWTZI'
+    'WwuvegGYqs89bDhUHZEnS9TBfXkkYq9LamlEVooR5kxb/kPtCnmMMXhQUOzH'
+    'xqakuZiN4AduRCzaecu0mearVjZWAChM3fYp4sMXKoRzek2F/xOUh81GxrW0'
+    'kbhpbaeXd6oG8p6AC3QCrEspzX78WEOCPSTJlx/BAv77A27b5zO2cSeZNbZq'
+    'XFqaQQj8AX46qoATWLhOnokoE2xeJTKikG/4nmc3D2KO6SRh66dEQWtJuVVw'
+    'ZqgQRdaseDjjgR1FKbC1'
+)
+
+SSH_RSA2 = (
+    'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBVmLXpTDhrYkABOPlADFk'
+    'GV8/QfgQqUQ0xn29hk18t/NTEQOW/Daq4EF84e9aTiopRXIk7jahBLzwWTZI'
+    'WwuvegGYqs89bDhUHZEnS9TBfXkkYq9LamlEVooR5kxb/kPtCnmMMXhQUOzH'
+    'xqakuZiN4AduRCzaecu0mearVjZWAChM3fYp4sMXKoRzek2F/xOUh81GxrW0'
+    'kbhpbaeXd6oG8p6AC3QCrEspzX78WEOCPSTJlx/BAv77A27b5zO2cSeZNbZq'
+    'XFqaQQj8AX46qoATWLhOnokoE2xeJTKikG/4nmc3D2KO6SRh66dEQWtJuVVw'
+    'ZqgQRdaseDjjgR1FK222'
+)
+
+SSH_DSA = (
+    'ssh-dss AAAAB3NzaC1kc3MAAACBAKSh2gHHQ0lsPEKZU7utlx3I/M8FtSMx'
+    '+MtE+QjReRPIWHjwTHLC6j5Bh2A8kwwiiqiiiDbvkJPgV3+5zmrnWvTICzet'
+    'zS4vOgk6ymDux2J/1JPRb6c2yjjFaYL0SndC6abdgohyUAJPzNkgEhnQll/o'
+    'QeavJXzLyonaX1wcl+R1AAAAFQCuMfl69Zyrx5B1qZmUsRVqG24W7wAAAIEA'
+    'pFVe4JOuhRjSufJXMV+nzoqkhIhDEOYLqcpnq3cUrvBFEkQ5tKyYephFJxq+'
+    'u7xkFx4d/K5eC7NH6/o/ziBocKJ7ESXBihC2lGLsHnWqreN9vCBihspBij+n'
+    '/wUpgcq2dMBDC2BzqCfdashM1xHm1XahqCvV87pvjRhl1avy+K0AAACAEQKs'
+    '3kKhEB/WGuAQa+tojRyIwtBc4lzZuJia4qOg6R4oSviKINwEtFtH08snteGn'
+    'c4qiZ6XBrfYJT2VS1yjFVj+OmGSHmrX1GdfRfco8Y1ZYC7VLwt20dutw9hKK'
+    'MSHI9NrJ5oOZ/GONlaKuqzKtTNb/vOIn/8yz52Od3X2Ehh1='
+)
diff --git a/ipatests/test_webui/test_user.py b/ipatests/test_webui/test_user.py
index 90e74eea1e..10e5a1f8b0 100644
--- a/ipatests/test_webui/test_user.py
+++ b/ipatests/test_webui/test_user.py
@@ -30,18 +30,33 @@
 import ipatests.test_webui.test_rbac as rbac
 import ipatests.test_webui.data_sudo as sudo
 import pytest
+import re
 
 try:
     from selenium.webdriver.common.by import By
+    from selenium.webdriver.common.keys import Keys
+    from selenium.webdriver.common.action_chains import ActionChains
 except ImportError:
     pass
 
-
+EMPTY_MOD = 'no modifications to be performed'
+USR_EXIST = 'user with name "{}" already exists'
+USR_ADDED = 'User successfully added'
+INVALID_SSH_KEY = "invalid 'sshpubkey': invalid SSH public key"
+INV_FIRSTNAME = ("invalid 'first': Leading and trailing spaces are "
+                 "not allowed")
+FIELD_REQ = 'Required field'
+ERR_INCLUDE = 'may only include letters, numbers, _, -, . and $'
+ERR_MISMATCH = 'Passwords must match'
+ERR_ADMIN_DEL = ('admin cannot be deleted or disabled because it is the last '
+                 'member of group admins')
 USR_EXIST = 'user with name "{}" already exists'
 ENTRY_EXIST = 'This entry already exists'
 ACTIVE_ERR = 'active user with name "{}" already exists'
 DISABLED = 'This entry is already disabled'
-
+LONG_LOGIN = "invalid 'login': can be at most 32 characters"
+INV_PASSWD = ("invalid 'password': Leading and trailing spaces are "
+              "not allowed")
 
 @pytest.mark.tier1
 class user_tasks(UI_driver):
@@ -50,6 +65,23 @@ def load_file(self, path):
             content = file_d.read()
         return content
 
+    def add_default_email_for_validation(self, data):
+        """
+        E-mail is generated automatically and we do not know domain yet in
+        data_user so in order to validate all mail fields we need to get it
+        there.
+        """
+        # getting domain from hostname due possible different DNS setup
+        domain = '.'.join(self.config.get('ipa_server').split('.')[1:])
+        user = data.get('pkey')
+        mail = '{}@{}'.format(user, domain)
+
+        for ele in data['mod_v']:
+            if 'mail' in ele:
+                ele[2].append(mail)
+
+        return data
+
 
 @pytest.mark.tier1
 class test_user(user_tasks):
@@ -60,7 +92,8 @@ def test_crud(self):
         Basic CRUD: user
         """
         self.init_app()
-        self.basic_crud(user.ENTITY, user.DATA)
+        data = self.add_default_email_for_validation(user.DATA)
+        self.basic_crud(user.ENTITY, data)
 
     @screenshot
     def test_associations(self):
@@ -376,6 +409,248 @@ def fill_password_dialog(self, password, current=None):
         self.wait_for_request(n=3)
         self.assert_no_error_dialog()
 
+    @screenshot
+    def test_login_without_username(self):
+
+        self.init_app(login='', password='xxx123', skip_username=True)
+
+        alert_e = self.find('.alert[data-name="username"]',
+                            By.CSS_SELECTOR)
+        assert 'Username: Required field' in alert_e.text, 'Alert expected'
+        assert self.login_screen_visible()
+
+    @screenshot
+    def test_disable_delete_admin(self):
+        """
+        Test disabling/deleting admin is not allowed
+        """
+        self.init_app()
+        self.navigate_to_entity('user')
+
+        # try to disable admin user
+        self.select_record('admin')
+        self.facet_button_click('disable')
+        self.dialog_button_click('ok')
+        self.assert_last_error_dialog(ERR_ADMIN_DEL, details=True)
+        self.dialog_button_click('ok')
+        self.assert_record('admin')
+
+        # try to delete admin user. Later we are
+        # confirming by keyboard to test also upstream ticket 4097
+        self.select_record('admin')
+        self.facet_button_click('remove')
+        self.dialog_button_click('ok')
+        self.assert_last_error_dialog(ERR_ADMIN_DEL, details=True)
+        actions = ActionChains(self.driver)
+        actions.send_keys(Keys.TAB)
+        actions.send_keys(Keys.ENTER).perform()
+        # self.dialog_button_click('ok')
+        self.wait(0.5)
+        self.assert_record('admin')
+
+    @screenshot
+    def test_add_user_special(self):
+        """
+        Test various add user special cases
+        """
+
+        self.init_app()
+
+        # Test invalid characters (#@*?) in login
+        self.navigate_to_entity('user')
+        self.facet_button_click('add')
+        self.fill_textbox('uid', 'itest-user#')
+        self.assert_field_validation(ERR_INCLUDE)
+        self.fill_textbox('uid', 'itest-user@')
+        self.assert_field_validation(ERR_INCLUDE)
+        self.fill_textbox('uid', 'itest-user*')
+        self.assert_field_validation(ERR_INCLUDE)
+        self.fill_textbox('uid', 'itest-user?')
+        self.assert_field_validation(ERR_INCLUDE)
+        self.dialog_button_click('cancel')
+
+        # Add an user with special chars
+        self.init_app()
+        self.basic_crud(user.ENTITY, user.DATA5)
+
+        # Add an user with long login (should FAIL)
+        self.init_app()
+        self.add_record(user.ENTITY, user.DATA6, negative=True)
+        self.assert_last_error_dialog(expected_err=LONG_LOGIN)
+        self.close_all_dialogs()
+
+        # Test password mismatch
+        self.add_record(user.ENTITY, user.DATA9, negative=True)
+        pass_e = self.find('.widget[name="userpassword2"]', By.CSS_SELECTOR)
+        self.assert_field_validation(ERR_MISMATCH, parent=pass_e)
+        self.dialog_button_click('cancel')
+        self.assert_record(user.DATA9.get('pkey'), negative=True)
+
+        # test add and edit record
+        self.add_record(user.ENTITY, user.DATA2, dialog_btn='add_and_edit')
+        self.action_list_action('delete_active_user')
+
+        # click add and cancel
+        self.add_record(user.ENTITY, user.DATA, dialog_btn='cancel')
+
+        # add leading space before password (should FAIL)
+        self.navigate_to_entity('user')
+        self.facet_button_click('add')
+        self.fill_fields(user.DATA7['add'])
+        self.dialog_button_click('add')
+        self.assert_last_error_dialog(INV_PASSWD)
+        self.close_all_dialogs()
+
+        # add trailing space before password (should FAIL)
+        self.navigate_to_entity('user')
+        self.facet_button_click('add')
+        self.fill_fields(user.DATA8['add'])
+        self.dialog_button_click('add')
+        self.assert_last_error_dialog(INV_PASSWD)
+        self.close_all_dialogs()
+
+        # add user using enter
+        self.add_record(user.ENTITY, user.DATA2, negative=True)
+        actions = ActionChains(self.driver)
+        actions.send_keys(Keys.ENTER).perform()
+        self.wait()
+        self.assert_notification(assert_text=USR_ADDED)
+        self.assert_record(user.PKEY2)
+
+        # delete user using enter
+        self.select_record(user.PKEY2)
+        self.facet_button_click('remove')
+        actions.send_keys(Keys.ENTER).perform()
+        self.wait(0.5)
+        self.assert_notification(assert_text='1 item(s) deleted')
+        self.assert_record(user.PKEY2, negative=True)
+
+    @screenshot
+    def test_add_delete_undo_reset_multivalue(self):
+        """
+        Test add and delete multivalue with reset and undo
+        """
+        self.init_app()
+
+        # getting domain from hostname due possible different DNS setup
+        domain = '.'.join(self.config.get('ipa_server').split('.')[1:])
+        first_mail = '{}@{}'.format(user.DATA.get('pkey'), domain)
+
+        self.add_record(user.ENTITY, user.DATA)
+        self.navigate_to_record(user.DATA.get('pkey'))
+
+        # add a new mail (without save) and reset
+        self.add_multivalued('mail', 't...@ipa.test')
+        self.assert_undo_button('mail')
+        self.facet_button_click('revert')
+        self.assert_undo_button('mail', visible=False)
+
+        # click at delete on the first mail and reset
+        self.del_multivalued('mail', first_mail)
+        self.assert_undo_button('mail')
+        self.facet_button_click('revert')
+        self.assert_undo_button('mail', visible=False)
+
+        # edit the first mail and reset
+        self.edit_multivalued('mail', first_mail, 't...@ipa.test')
+        self.assert_undo_button('mail')
+        self.facet_button_click('revert')
+        self.assert_undo_button('mail', visible=False)
+
+        # add a new mail and undo
+        self.add_multivalued('mail', 't...@ipa.test')
+        self.assert_undo_button('mail')
+        self.undo_multivalued('mail', 't...@ipa.test')
+        self.assert_undo_button('mail', visible=False)
+
+        # edit the first mail and undo
+        self.edit_multivalued('mail', first_mail, 't...@ipa.test')
+        self.assert_undo_button('mail')
+        self.undo_multivalued('mail', 't...@ipa.test')
+        self.assert_undo_button('mail', visible=False)
+
+        # cleanup
+        self.delete_record(user.DATA.get('pkey'))
+
+    def test_user_misc(self):
+        """
+        Test various miscellaneous test cases under one roof to save init time
+        """
+        self.init_app()
+
+        # add already existing user (should fail) / also test ticket 4098
+        self.add_record(user.ENTITY, user.DATA)
+        self.add_record(user.ENTITY, user.DATA, negative=True,
+                        pre_delete=False)
+        self.assert_last_error_dialog(USR_EXIST.format(user.PKEY))
+        actions = ActionChains(self.driver)
+        actions.send_keys(Keys.TAB)
+        actions.send_keys(Keys.ENTER).perform()
+        self.wait(0.5)
+        self.dialog_button_click('cancel')
+
+        # add user without login name
+        self.add_record(user.ENTITY, user.DATA10)
+        self.assert_record('nsurname10')
+
+        # try to add same user without login name again (should fail)
+        self.add_record(user.ENTITY, user.DATA10, negative=True,
+                        pre_delete=False)
+        self.assert_last_error_dialog(USR_EXIST.format('nsurname10'))
+        self.close_all_dialogs()
+
+        # try to modify user`s UID to -1 (should fail)
+        self.navigate_to_record(user.PKEY)
+        self.mod_record(
+            user.ENTITY, {'mod': [('textbox', 'uidnumber', '-1')]},
+            negative=True)
+        uid_e = self.find('.widget[name="uidnumber"]', By.CSS_SELECTOR)
+        self.assert_field_validation('Minimum value is 1', parent=uid_e)
+        self.facet_button_click('revert')
+
+        # edit user`s "First name" to value with leading space (should fail)
+        self.fill_input('givenname', ' leading_space')
+        self.facet_button_click('save')
+        self.assert_last_error_dialog(INV_FIRSTNAME)
+        self.dialog_button_click('cancel')
+
+        # edit user`s "First name" to value with trailing space (should fail)
+        self.fill_input('givenname', 'trailing_space ')
+        self.facet_button_click('save')
+        self.assert_last_error_dialog(INV_FIRSTNAME)
+        self.dialog_button_click('cancel')
+
+        # try with blank "First name" (should fail)
+        gn_input_s = "input[type='text'][name='givenname']"
+        gn_input_el = self.find(gn_input_s, By.CSS_SELECTOR, strict=True)
+        gn_input_el.clear()
+        gn_input_el.send_keys(Keys.BACKSPACE)
+
+        self.facet_button_click('save')
+        gn_e = self.find('.widget[name="givenname"]', By.CSS_SELECTOR)
+        self.assert_field_validation(FIELD_REQ, parent=gn_e)
+        self.close_notifications()
+
+        # check we can see version in "profile > about", ticket 4018
+        self.profile_menu_action('about')
+
+        about_text = self.get_text('div[data-name="version_dialog"] p')
+        ver_re = re.compile('version:\s.*')
+        assert re.search(ver_re, about_text), 'Version not found'
+        self.dialog_button_click('ok')
+
+        # search user / multiple users
+        self.navigate_to_entity('user')
+        self.wait(0.5)
+        self.find_record('user', user.DATA)
+        self.add_record(user.ENTITY, user.DATA2)
+        self.find_record('user', user.DATA2)
+        # search for both users (just 'itest-user' will do)
+        self.find_record('user', user.DATA)
+        self.assert_record(user.PKEY2)
+
+        # cleanup
+        self.delete_record([user.PKEY, user.PKEY2, user.PKEY10, 'nsurname10'])
 
 @pytest.mark.tier1
 class test_user_no_private_group(UI_driver):
@@ -588,6 +863,164 @@ def test_life_cycles(self):
         self.assert_record_value('Enabled', [user.PKEY, user.PKEY2],
                                  'nsaccountlock')
 
-        # cleanup
+        # cleanup and check for ticket 4245 (select all should not remain
+        # checked after delete action
         self.navigate_to_entity('user')
-        self.delete_record([user.PKEY, user.PKEY2])
+        select_all_btn = self.find('input[title="Select All"]',
+                                   By.CSS_SELECTOR)
+        select_all_btn.click()
+        self.facet_button_click('remove')
+        self.dialog_button_click('ok')
+        self.dialog_button_click('ok')
+        self.assert_value_checked('admin', 'uid', negative=True)
+
+
+@pytest.mark.tier1
+class TestSSHkeys(UI_driver):
+
+    def assert_num_ssh_keys(self, num):
+        """
+        Assert number of SSH keys we have associated with the user
+        """
+
+        s_keys = 'div[name="ipasshpubkey"] .widget[name="value"]'
+        ssh_keys = self.find(s_keys, By.CSS_SELECTOR, many=True)
+
+        num_ssh_keys = len(ssh_keys) if not None else 0
+
+        assert num_ssh_keys == num, \
+            ('Number of SSH keys does not match. '
+             'Expected: {}, Got: {}'.format(num, num_ssh_keys))
+
+    def undo_ssh_keys(self, btn_name='undo'):
+        """
+        Undo either one SSH key or all of them
+
+        Possible options:
+        btn_name='undo'
+        btn_name='undo_all'
+        """
+
+        s_undo = 'div[name="ipasshpubkey"] button[name="{}"]'.format(btn_name)
+        undo = self.find(s_undo, By.CSS_SELECTOR, strict=True)
+        undo.click()
+        self.wait(0.6)
+
+    @screenshot
+    def test_ssh_keys(self):
+
+        self.init_app()
+
+        # add and undo SSH key
+        self.add_sshkey_to_user(user.SSH_RSA, save=False)
+        self.assert_num_ssh_keys(1)
+        self.undo_ssh_keys()
+        self.assert_num_ssh_keys(0)
+
+        # add and undo 2 SSH keys (using undo all)
+        ssh_keys = [user.SSH_RSA, user.SSH_DSA]
+
+        self.add_sshkey_to_user(ssh_keys, save=False)
+        self.assert_num_ssh_keys(2)
+        self.undo_ssh_keys(btn_name='undo_all')
+        self.assert_num_ssh_keys(0)
+
+        # add SSH key and refresh
+        self.add_sshkey_to_user(user.SSH_RSA, save=False)
+        self.assert_num_ssh_keys(1)
+        self.facet_button_click('refresh')
+        self.assert_num_ssh_keys(0)
+
+        # add SSH key and revert
+        self.add_sshkey_to_user(user.SSH_RSA, save=False)
+        self.assert_num_ssh_keys(1)
+        self.facet_button_click('revert')
+        self.assert_num_ssh_keys(0)
+
+        # add SSH key, move elsewhere and cancel.
+        self.add_sshkey_to_user(user.SSH_RSA, save=False)
+        self.assert_num_ssh_keys(1)
+        self.switch_to_facet('memberof_group')
+        self.dialog_button_click('cancel')
+        self.assert_num_ssh_keys(1)
+        self.undo_ssh_keys()
+
+        # add SSH key, move elsewhere and click reset button.
+        self.add_sshkey_to_user(user.SSH_RSA, save=False)
+        self.assert_num_ssh_keys(1)
+        self.switch_to_facet('memberof_group')
+        self.wait_for_request()
+        self.dialog_button_click('revert')
+        self.wait()
+        self.switch_to_facet('details')
+        self.assert_num_ssh_keys(0)
+
+        # add SSH key, move elsewhere and click save button.
+        self.add_sshkey_to_user(user.SSH_RSA, save=False)
+        self.assert_num_ssh_keys(1)
+        self.switch_to_facet('memberof_group')
+        self.wait()
+        self.dialog_button_click('save')
+        self.wait_for_request(n=4)
+        self.switch_to_facet('details')
+        self.assert_num_ssh_keys(1)
+        self.delete_user_sshkeys(navigate=False)
+
+        # add,save and delete RSA and DSA keys
+        keys = [user.SSH_RSA, user.SSH_DSA]
+
+        self.add_sshkey_to_user(keys)
+        self.assert_num_ssh_keys(2)
+        self.delete_user_sshkeys()
+        self.assert_num_ssh_keys(0)
+
+        # add RSA SSH keys with trailing space and "=" sign at the end
+        keys = [user.SSH_RSA+" ", user.SSH_RSA2+"="]
+
+        self.add_sshkey_to_user(keys)
+        self.assert_num_ssh_keys(2)
+        self.delete_user_sshkeys()
+        self.assert_num_ssh_keys(0)
+
+        # lets try to add empty SSH key (should fail)
+        self.add_sshkey_to_user('')
+        self.assert_last_error_dialog(EMPTY_MOD)
+        self.dialog_button_click('cancel')
+        self.undo_ssh_keys()
+
+        # try to add invalid SSH key
+        self.add_sshkey_to_user('invalid_key')
+        self.assert_last_error_dialog(INVALID_SSH_KEY)
+        self.dialog_button_click('cancel')
+        self.undo_ssh_keys()
+
+        # add duplicate SSH keys
+        self.add_sshkey_to_user(user.SSH_RSA)
+        self.add_sshkey_to_user(user.SSH_RSA, save=False)
+        self.facet_button_click('save')
+        self.assert_last_error_dialog(EMPTY_MOD)
+        self.dialog_button_click('cancel')
+
+        # test SSH key edit when user lacks write rights for related attribute
+        # see ticket 3800
+        self.add_record(user.ENTITY, [user.DATA2, user.DATA5])
+        self.add_sshkey_to_user(user.SSH_RSA, user.PKEY2)
+
+        self.logout()
+        self.init_app(user.PKEY5, '!!!@@@###$$$')
+
+        self.navigate_to_record(user.PKEY2, entity=user.ENTITY)
+
+        show_ssh_key_btn = self.find('div.widget .btn[name="ipasshpubkey-0"]',
+                                 By.CSS_SELECTOR)
+        show_ssh_key_btn.click()
+        ssh_key_dialog = self.find('.modal-dialog textarea', By.CSS_SELECTOR)
+
+        assert ssh_key_dialog.get_attribute('readonly') == 'true'
+        self.dialog_button_click('cancel')
+        self.logout()
+        self.init_app()
+
+        # cleanup
+        self.delete_record([user.PKEY2, user.PKEY5])
+        self.delete_user_sshkeys()

From 0f1729e6576d131495bacf83b2b32a8e14a9e03a Mon Sep 17 00:00:00 2001
From: Michal Reznik <mrez...@redhat.com>
Date: Thu, 19 Apr 2018 15:27:42 +0200
Subject: [PATCH 2/3] ui_driver: extension and modifications related to
 test_user

In this patch we tune init_app() and login() in order to test
login without username.

Then we add edit_multivalued and undo_multivalued to test "undo"
and "reset" buttons.

Also there is a new boolean "negative" in mod_record() to switch
button assertion.

Later ssh_key methods were fine-tuned a little to add more keys
and delete all of them.

Lastly new method assert_value_checked() was introduced to assert
whether a particular record is checked.

https://pagure.io/freeipa/issue/7507
---
 ipatests/test_webui/ui_driver.py | 126 +++++++++++++++++++++++++++++++++------
 1 file changed, 107 insertions(+), 19 deletions(-)

diff --git a/ipatests/test_webui/ui_driver.py b/ipatests/test_webui/ui_driver.py
index 65cd66afe2..0cc1b9f2ed 100644
--- a/ipatests/test_webui/ui_driver.py
+++ b/ipatests/test_webui/ui_driver.py
@@ -340,13 +340,13 @@ def xpath_has_val(self, attr, val):
         """
         return "contains(concat(' ',normalize-space(@%s), ' '),' %s ')" % (attr, val)
 
-    def init_app(self, login=None, password=None):
+    def init_app(self, login=None, password=None, skip_username=False):
         """
         Load and login
         """
         self.load()
         self.wait(0.5)
-        self.login(login, password)
+        self.login(login, password, skip_username=skip_username)
         # metadata + default page
         self.wait_for_request(n=5)
 
@@ -377,7 +377,8 @@ def load(self):
         WebDriverWait(self.driver, 10).until(lambda d: runner.files_loaded())
         self.wait_for_request()
 
-    def login(self, login=None, password=None, new_password=None):
+    def login(self, login=None, password=None, new_password=None,
+              skip_username=False):
         """
         Log in if user is not logged in.
         """
@@ -390,6 +391,8 @@ def login(self, login=None, password=None, new_password=None):
             password = self.config['ipa_password']
         if not new_password:
             new_password = password
+        if skip_username:
+            login = ''
 
         auth = self.get_login_screen()
         login_tb = self.find("//input[@type='text'][@name='username']",
@@ -424,6 +427,8 @@ def logged_in(self):
 
     def logout(self):
         self.profile_menu_action('logout')
+        # it may take some time to get login screen visible
+        self.wait(5)
         assert self.login_screen_visible()
 
     def get_login_screen(self):
@@ -815,6 +820,46 @@ def add_multivalued(self, name, value, parent=None):
         last = inputs[-1]
         last.send_keys(value)
 
+    def edit_multivalued(self, name, value, new_value, parent=None):
+        """
+        Edit multivalued textbox
+        """
+        if not parent:
+            parent = self.get_form()
+        s = "div[name='%s'].multivalued-widget" % name
+        w = self.find(s, By.CSS_SELECTOR, parent, strict=True)
+        s = "div[name=value] input"
+        inputs = self.find(s, By.CSS_SELECTOR, w, many=True)
+
+        for i in inputs:
+            val = i.get_attribute('value')
+            if val == value:
+                i.clear()
+                i.send_keys(new_value)
+
+    def undo_multivalued(self, name, value, parent=None):
+        """
+        Undo multivalued change
+        """
+        if not parent:
+            parent = self.get_form()
+        s = "div[name='%s'].multivalued-widget" % name
+        w = self.find(s, By.CSS_SELECTOR, parent, strict=True)
+        s = "div[name=value] input"
+        inputs = self.find(s, By.CSS_SELECTOR, w, many=True)
+        clicked = False
+        for i in inputs:
+            val = i.get_attribute('value')
+            n = i.get_attribute('name')
+            if val == value:
+                s = "input[name='%s'] ~ .input-group-btn button[name=undo]" % n
+                link = self.find(s, By.CSS_SELECTOR, w, strict=True)
+                link.click()
+                self.wait()
+                clicked = True
+
+        assert clicked, 'Value was not undone: %s' % value
+
     def del_multivalued(self, name, value, parent=None):
         """
         Mark value in multivalued textbox as deleted.
@@ -1369,7 +1414,8 @@ def add_record(self, entity, data, facet='search', facet_btn='add',
             new_count = len(self.get_rows())
             self.assert_row_count(count, new_count)
 
-    def mod_record(self, entity, data, facet='details', facet_btn='save'):
+    def mod_record(self, entity, data, facet='details', facet_btn='save',
+                   negative=False):
         """
         Mod record
 
@@ -1384,6 +1430,9 @@ def mod_record(self, entity, data, facet='details', facet_btn='save'):
         self.facet_button_click(facet_btn)
         self.wait_for_request()
         self.wait_for_request()
+
+        if negative:
+            return
         self.assert_facet_button_enabled(facet_btn, enabled=False)
 
     def basic_crud(self, entity, data,
@@ -1700,32 +1749,53 @@ def get_t_vals(t):
             # add multiple at once and test table delete button
             self.add_table_associations(table, keys, delete=True)
 
-    def add_sshkey_to_user(self, user, ssh_key):
+    def add_sshkey_to_user(self, ssh_keys, user='admin', navigate=True,
+                           save=True):
         """
         Add ssh public key to particular user
 
+        ssh_keys (list): public ssh key(s)
         user (str): user to add the key to
-        ssh_key (str): public ssh key
         """
-        self.navigate_to_entity('user')
-        self.navigate_to_record(user)
 
-        ssh_pub = 'div[name="ipasshpubkey"] button[name="add"]'
-        self.find(ssh_pub, By.CSS_SELECTOR).click()
-        self.wait()
-        self.driver.switch_to.active_element.send_keys(ssh_key)
-        self.dialog_button_click('update')
-        self.facet_button_click('save')
+        if type(ssh_keys) is not list:
+            ssh_keys = [ssh_keys]
+
+        if navigate:
+            self.navigate_to_entity('user')
+            self.navigate_to_record(user)
+
+        for key in ssh_keys:
+            s_add = 'div[name="ipasshpubkey"] button[name="add"]'
+            ssh_add_btn = self.find(s_add, By.CSS_SELECTOR, strict=True)
+            ssh_add_btn.click()
+            self.wait()
+            s_text_area = 'textarea.certificate'
+            text_area = self.find(s_text_area, By.CSS_SELECTOR, strict=True)
+            text_area.send_keys(key)
+            self.wait()
+            self.dialog_button_click('update')
 
-    def delete_user_sshkey(self, user):
+        # sometimes we do not want to save e.g. in order to test undo buttons
+        if save:
+            self.facet_button_click('save')
+
+    def delete_user_sshkeys(self, user='admin', navigate=True):
         """
-        Delete ssh public key of particular user
+        Delete all ssh public keys of particular user
         """
-        self.navigate_to_entity('user')
-        self.navigate_to_record(user)
+
+        if navigate:
+            self.navigate_to_entity('user')
+            self.navigate_to_record(user)
 
         ssh_pub = 'div[name="ipasshpubkey"] button[name="remove"]'
-        self.find(ssh_pub, By.CSS_SELECTOR).click()
+        rm_btns = self.find(ssh_pub, By.CSS_SELECTOR, many=True)
+        assert rm_btns, 'No SSH keys to be deleted'
+
+        for btn in rm_btns:
+            btn.click()
+
         self.facet_button_click('save')
 
     def run_cmd_on_ui_host(self, cmd):
@@ -2080,3 +2150,21 @@ def assert_last_error_dialog(self, expected_err, details=False,
         else:
             s = '.modal-body div p'
             self.assert_text(s, expected_err, parent=err_dialog)
+
+    def assert_value_checked(self, values, name, negative=False):
+        """
+        Assert particular value is checked
+        """
+
+        if type(values) is not list:
+            values = [values]
+
+        checked_values = self.get_field_checked(name)
+
+        for value in values:
+            if negative:
+                assert not value in checked_values, ('{} checked while it '
+                                                     'should not be'.format(value))
+            else:
+                assert value in checked_values, ('{} NOT checked while it '
+                                                     'should be'.format(value))

From f1fd6f76d289fabd5ff0032c36e6bbfddec53cf9 Mon Sep 17 00:00:00 2001
From: Michal Reznik <mrez...@redhat.com>
Date: Thu, 19 Apr 2018 16:40:28 +0200
Subject: [PATCH 3/3] temp_commit: activate PRCI WebUI tests

---
 .freeipa-pr-ci.yaml | 158 +++-------------------------------------------------
 1 file changed, 9 insertions(+), 149 deletions(-)

diff --git a/.freeipa-pr-ci.yaml b/.freeipa-pr-ci.yaml
index c95bef79e2..0a9b48719f 100644
--- a/.freeipa-pr-ci.yaml
+++ b/.freeipa-pr-ci.yaml
@@ -3,6 +3,10 @@ topologies:
     name: build
     cpu: 2
     memory: 3800
+  ipaserver: &ipaserver
+    name: ipaserver
+    cpu: 1
+    memory: 2400
   master_1repl: &master_1repl
     name: master_1repl
     cpu: 4
@@ -27,158 +31,14 @@ jobs:
         timeout: 1800
         topology: *build
 
-  fedora-27/simple_replication:
+  fedora-27/test_webui:
     requires: [fedora-27/build]
     priority: 50
     job:
-      class: RunPytest
+      class: RunWebuiTests
       args:
         build_url: '{fedora-27/build_url}'
-        test_suite: test_integration/test_simple_replication.py
+        test_suite: test_webui/
         template: *ci-master-f27
-        timeout: 3600
-        topology: *master_1repl
-
-  fedora-27/caless:
-    requires: [fedora-27/build]
-    priority: 50
-    job:
-      class: RunPytest
-      args:
-        build_url: '{fedora-27/build_url}'
-        test_suite: test_integration/test_caless.py::TestServerReplicaCALessToCAFull
-        template: *ci-master-f27
-        timeout: 3600
-        topology: *master_1repl
-
-  fedora-27/external_ca:
-    requires: [fedora-27/build]
-    priority: 50
-    job:
-      class: RunPytest
-      args:
-        build_url: '{fedora-27/build_url}'
-        test_suite: test_integration/test_external_ca.py
-        template: *ci-master-f27
-        timeout: 3600
-        topology: *master_1repl
-
-  fedora-27/test_topologies:
-    requires: [fedora-27/build]
-    priority: 50
-    job:
-      class: RunPytest
-      args:
-        build_url: '{fedora-27/build_url}'
-        test_suite: test_integration/test_topologies.py
-        template: *ci-master-f27
-        timeout: 3600
-        topology: *master_1repl
-
-  fedora-27/test_sudo:
-    requires: [fedora-27/build]
-    priority: 50
-    job:
-      class: RunPytest
-      args:
-        build_url: '{fedora-27/build_url}'
-        test_suite: test_integration/test_sudo.py
-        template: *ci-master-f27
-        timeout: 3600
-        topology: *master_1repl_1client
-
-  fedora-27/test_kerberos_flags:
-    requires: [fedora-27/build]
-    priority: 50
-    job:
-      class: RunPytest
-      args:
-        build_url: '{fedora-27/build_url}'
-        test_suite: test_integration/test_kerberos_flags.py
-        template: *ci-master-f27
-        timeout: 3600
-        topology: *master_1repl_1client
-
-  fedora-27/test_http_kdc_proxy:
-    requires: [fedora-27/build]
-    priority: 50
-    job:
-      class: RunPytest
-      args:
-        build_url: '{fedora-27/build_url}'
-        test_suite: test_integration/test_http_kdc_proxy.py
-        template: *ci-master-f27
-        timeout: 3600
-        topology: *master_1repl_1client
-
-  fedora-27/test_forced_client_enrolment:
-    requires: [fedora-27/build]
-    priority: 50
-    job:
-      class: RunPytest
-      args:
-        build_url: '{fedora-27/build_url}'
-        test_suite: test_integration/test_forced_client_reenrollment.py
-        template: *ci-master-f27
-        timeout: 3600
-        topology: *master_1repl_1client
-
-  fedora-27/test_advise:
-    requires: [fedora-27/build]
-    priority: 50
-    job:
-      class: RunPytest
-      args:
-        build_url: '{fedora-27/build_url}'
-        test_suite: test_integration/test_advise.py
-        template: *ci-master-f27
-        timeout: 3600
-        topology: *master_1repl
-
-  fedora-27/test_testconfig:
-    requires: [fedora-27/build]
-    priority: 50
-    job:
-      class: RunPytest
-      args:
-        build_url: '{fedora-27/build_url}'
-        test_suite: test_integration/test_testconfig.py
-        template: *ci-master-f27
-        timeout: 3600
-        topology: *master_1repl
-
-  fedora-27/test_service_permissions:
-    requires: [fedora-27/build]
-    priority: 50
-    job:
-      class: RunPytest
-      args:
-        build_url: '{fedora-27/build_url}'
-        test_suite: test_integration/test_service_permissions.py
-        template: *ci-master-f27
-        timeout: 3600
-        topology: *master_1repl
-
-  fedora-27/test_netgroup:
-    requires: [fedora-27/build]
-    priority: 50
-    job:
-      class: RunPytest
-      args:
-        build_url: '{fedora-27/build_url}'
-        test_suite: test_integration/test_netgroup.py
-        template: *ci-master-f27
-        timeout: 3600
-        topology: *master_1repl
-
-  fedora-27/test_vault:
-    requires: [fedora-27/build]
-    priority: 50
-    job:
-      class: RunPytest
-      args:
-        build_url: '{fedora-27/build_url}'
-        test_suite: test_integration/test_vault.py
-        template: *ci-master-f27
-        timeout: 3600
-        topology: *master_1repl
+        timeout: 10000
+        topology: *ipaserver
_______________________________________________
FreeIPA-devel mailing list -- freeipa-devel@lists.fedorahosted.org
To unsubscribe send an email to freeipa-devel-le...@lists.fedorahosted.org

Reply via email to