URL: https://github.com/freeipa/freeipa/pull/1869
Author: Rezney
 Title: #1869: [Backport][ipa-4-6] ui_tests: extend test_user suite
Action: opened

PR body:
"""
This PR was opened automatically because PR #1838 was pushed to master and 
backport to ipa-4-6 is required.
"""

To pull the PR as Git branch:
git remote add ghfreeipa https://github.com/freeipa/freeipa
git fetch ghfreeipa pull/1869/head:pr1869
git checkout pr1869
From ac46e6f069f248c20e26a9a08d43661af49ab0fe 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 | 154 +++++++++++++++
 ipatests/test_webui/test_user.py | 416 ++++++++++++++++++++++++++++++++++++++-
 2 files changed, 563 insertions(+), 7 deletions(-)

diff --git a/ipatests/test_webui/data_user.py b/ipatests/test_webui/data_user.py
index c5ed796c7b..ae62f72610 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'),
     ],
 }
 
@@ -85,3 +136,106 @@
         ('combobox', 'gidnumber', '77777'),
     ]
 }
+
+PKEY_SPECIAL_CHARS = '1spe.cial_us-er$'
+PASSWD_SCECIAL_CHARS = '!!!@@@###$$$'
+DATA_SPECIAL_CHARS = {
+    'pkey': PKEY_SPECIAL_CHARS,
+    'add': [
+        ('textbox', 'uid', PKEY_SPECIAL_CHARS),
+        ('textbox', 'givenname', 'S$p|e>c--i_a%l_'),
+        ('textbox', 'sn', '%U&s?e+r'),
+        ('password', 'userpassword', PASSWD_SCECIAL_CHARS),
+        ('password', 'userpassword2', PASSWD_SCECIAL_CHARS),
+    ]
+}
+
+PKEY_LONG_LOGIN = 'itest-user' * 5
+DATA_LONG_LOGIN = {
+    'pkey': PKEY_LONG_LOGIN,
+    'add': [
+        ('textbox', 'uid', PKEY_LONG_LOGIN),
+        ('textbox', 'givenname', 'Name8'),
+        ('textbox', 'sn', 'Surname8'),
+    ]
+}
+
+PKEY_PASSWD_LEAD_SPACE = 'itest-user-passwd-leading-space'
+DATA_PASSWD_LEAD_SPACE = {
+    'pkey': PKEY_PASSWD_LEAD_SPACE,
+    'add': [
+        ('textbox', 'uid', PKEY_PASSWD_LEAD_SPACE),
+        ('textbox', 'givenname', 'Name7'),
+        ('textbox', 'sn', 'Surname7'),
+        ('password', 'userpassword', ' Password123 '),
+        ('password', 'userpassword2', ' Password123 '),
+    ]
+}
+
+PKEY_PASSWD_TRAIL_SPACE = 'itest-user-passwd-trailing-space'
+DATA_PASSWD_TRAIL_SPACE = {
+    'pkey': PKEY_PASSWD_LEAD_SPACE,
+    'add': [
+        ('textbox', 'uid', PKEY_PASSWD_LEAD_SPACE),
+        ('textbox', 'givenname', 'Name8'),
+        ('textbox', 'sn', 'Surname8'),
+        ('password', 'userpassword', 'Password123 '),
+        ('password', 'userpassword2', 'Password123 '),
+    ]
+}
+
+PKEY_PASSWD_MISMATCH = 'itest-user-passwd-mismatch'
+DATA_PASSWD_MISMATCH = {
+    'pkey': PKEY_PASSWD_MISMATCH,
+    'add': [
+        ('textbox', 'uid', PKEY_PASSWD_MISMATCH),
+        ('textbox', 'givenname', 'Name9'),
+        ('textbox', 'sn', 'Surname9'),
+        ('password', 'userpassword', 'Password123'),
+        ('password', 'userpassword2', 'Password12'),
+    ]
+}
+
+PKEY_NO_LOGIN = 'itest-user-no-login'
+DATA_NO_LOGIN = {
+    'pkey': PKEY_NO_LOGIN,
+    '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..00995744da 100644
--- a/ipatests/test_webui/test_user.py
+++ b/ipatests/test_webui/test_user.py
@@ -33,15 +33,29 @@
 
 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 +64,29 @@ def load_file(self, path):
             content = file_d.read()
         return content
 
+    def create_email_addr(self, pkey):
+        """
+        Piece an email address together from hostname due possible different
+        DNS setup
+        """
+
+        domain = '.'.join(self.config.get('ipa_server').split('.')[1:])
+        return '{}@{}'.format(pkey, domain)
+
+    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.
+        """
+        mail = self.create_email_addr(user.DATA.get('pkey'))
+
+        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 +97,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 +414,237 @@ 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')
+
+        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.ENTITY)
+
+        # 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 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.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.ENTITY)
+        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.basic_crud(user.ENTITY, user.DATA_SPECIAL_CHARS)
+
+        # Add an user with long login (should FAIL)
+        self.add_record(user.ENTITY, user.DATA_LONG_LOGIN, negative=True)
+        self.assert_last_error_dialog(expected_err=LONG_LOGIN)
+        self.close_all_dialogs()
+
+        # Test password mismatch
+        self.add_record(user.ENTITY, user.DATA_PASSWD_MISMATCH, 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.DATA_PASSWD_MISMATCH.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.ENTITY)
+        self.facet_button_click('add')
+        self.fill_fields(user.DATA_PASSWD_LEAD_SPACE['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.ENTITY)
+        self.facet_button_click('add')
+        self.fill_fields(user.DATA_PASSWD_TRAIL_SPACE['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)
+        self.close_notifications()
+
+        # 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()
+
+        first_mail = self.create_email_addr(user.DATA.get('pkey'))
+
+        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(user.ENTITY, [user.DATA])
+
+    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.DATA_NO_LOGIN)
+        self.assert_record('nsurname10')
+
+        # try to add same user without login name again (should fail)
+        self.add_record(user.ENTITY, user.DATA_NO_LOGIN, 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()
+
+        # search user / multiple users
+        self.navigate_to_entity(user.ENTITY)
+        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.PKEY_NO_LOGIN,
+                            'nsurname10'])
 
 @pytest.mark.tier1
 class test_user_no_private_group(UI_driver):
@@ -510,7 +779,7 @@ def test_life_cycles(self):
 
         # send multiple records to preserved
         self.navigate_to_entity('stageuser')
-        self.navigate_to_entity('user')
+        self.navigate_to_entity(user.ENTITY)
         self.delete_record([user.PKEY, user.PKEY2],
                            confirm_btn=None)
         self.check_option('preserve', value='true')
@@ -527,7 +796,7 @@ def test_life_cycles(self):
         self.wait()
 
         # send multiple users to staged (through preserved)
-        self.navigate_to_entity('user')
+        self.navigate_to_entity(user.ENTITY)
         self.delete_record([user.PKEY, user.PKEY2],
                            confirm_btn=None)
         self.check_option('preserve', value='true')
@@ -588,6 +857,139 @@ def test_life_cycles(self):
         self.assert_record_value('Enabled', [user.PKEY, user.PKEY2],
                                  'nsaccountlock')
 
+        # cleanup and check for ticket 4245 (select all should not remain
+        # checked after delete action). Two "ok" buttons at the end are needed
+        # for delete confirmation and acknowledging that "admin" cannot be
+        # deleted.
+        self.navigate_to_entity(user.ENTITY)
+        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):
+
+    @screenshot
+    def test_ssh_keys(self):
+
+        self.init_app()
+
+        # add and undo SSH key
+        self.add_sshkey_to_record(user.SSH_RSA, 'admin', save=False,
+                                  navigate=True)
+        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_record(ssh_keys, 'admin', 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_record(user.SSH_RSA, 'admin', 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_record(user.SSH_RSA, 'admin', 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_record(user.SSH_RSA, 'admin', 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_record(user.SSH_RSA, 'admin', 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_record(user.SSH_RSA, 'admin', 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_record_sshkeys('admin')
+
+        # add, save and delete RSA and DSA keys
+        keys = [user.SSH_RSA, user.SSH_DSA]
+
+        self.add_sshkey_to_record(keys, 'admin')
+        self.assert_num_ssh_keys(2)
+        self.delete_record_sshkeys('admin')
+        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_record(keys, 'admin')
+        self.assert_num_ssh_keys(2)
+        self.delete_record_sshkeys('admin')
+        self.assert_num_ssh_keys(0)
+
+        # lets try to add empty SSH key (should fail)
+        self.add_sshkey_to_record('', 'admin')
+        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_record('invalid_key', 'admin')
+        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_record(user.SSH_RSA, 'admin')
+        self.add_sshkey_to_record(user.SSH_RSA, 'admin', 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 (we use DATA_SPECIAL_CHARS just for convenience)
+        self.add_record(user.ENTITY, [user.DATA2, user.DATA_SPECIAL_CHARS])
+        self.add_sshkey_to_record(user.SSH_RSA, user.PKEY2, navigate=True)
+
+        self.logout()
+        self.init_app(user.PKEY_SPECIAL_CHARS, user.PASSWD_SCECIAL_CHARS)
+
+        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_e = self.find('textarea', By.CSS_SELECTOR, self.get_dialog())
+
+        assert ssh_key_e.get_attribute('readonly') == 'true'
+        self.dialog_button_click('cancel')
+        self.logout()
+        self.init_app()
+
         # cleanup
-        self.navigate_to_entity('user')
-        self.delete_record([user.PKEY, user.PKEY2])
+        self.delete(user.ENTITY, [user.DATA2, user.DATA_SPECIAL_CHARS])
+        self.delete_record_sshkeys('admin', navigate=True)

From c969c5df2951799c5c8e5f6800e675f7bf002c3a 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 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,
delete all of them and to extend their usage to hosts and id views.

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 | 171 ++++++++++++++++++++++++++++++++++-----
 1 file changed, 150 insertions(+), 21 deletions(-)

diff --git a/ipatests/test_webui/ui_driver.py b/ipatests/test_webui/ui_driver.py
index 70fe570352..2991eb90fc 100644
--- a/ipatests/test_webui/ui_driver.py
+++ b/ipatests/test_webui/ui_driver.py
@@ -59,7 +59,6 @@
     NO_YAML = True
 from ipaplatform.paths import paths
 
-
 ENV_MAP = {
     'MASTER': 'ipa_server',
     'ADMINID': 'ipa_admin',
@@ -374,9 +373,9 @@ def login(self, login=None, password=None, new_password=None):
         if self.logged_in():
             return
 
-        if not login:
+        if login is None:
             login = self.config['ipa_admin']
-        if not password:
+        if password is None:
             password = self.config['ipa_password']
         if not new_password:
             new_password = password
@@ -413,7 +412,14 @@ def logged_in(self):
         return logged_in
 
     def logout(self):
+
+        runner = self
+
         self.profile_menu_action('logout')
+        # it may take some time to get login screen visible
+        WebDriverWait(self.driver, self.request_timeout).until(
+            lambda d: runner.login_screen_visible())
+
         assert self.login_screen_visible()
 
     def get_login_screen(self):
@@ -805,6 +811,50 @@ 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
+                # lets try to find the undo button element again to check if
+                # it is not present or displayed
+                link = self.find(s, By.CSS_SELECTOR, w)
+                assert not link or not link.is_displayed(), 'Undo btn present'
+
+        assert clicked, 'Value was not undone: %s' % value
+
     def del_multivalued(self, name, value, parent=None):
         """
         Mark value in multivalued textbox as deleted.
@@ -1359,7 +1409,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
 
@@ -1374,6 +1425,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,
@@ -1690,34 +1744,90 @@ 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_record(self, ssh_keys, pkey, entity='user',
+                             navigate=False, save=True):
         """
-        Add ssh public key to particular user
+        Add ssh public key to particular record
 
-        user (str): user to add the key to
-        ssh_key (str): public ssh key
+        ssh_keys (list): public ssh key(s)
+        pkey (str): user/host/idview to add the key to
+        entity (str): name of entity where to navigate if navigate=True
+        navigate (bool): whether we should navigate to record
+        save (bool): whether we should click save after adding a 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(entity)
+            self.navigate_to_record(pkey)
+
+        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')
+
+        # 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_sshkey(self, user):
+    def delete_record_sshkeys(self, pkey, entity='user', navigate=False):
         """
-        Delete ssh public key of particular user
+        Delete all ssh public keys of particular record
+
+        pkey (str): user/host/idview to add the key to
+        entity (str): name of entity where to navigate if navigate=True
+        navigate (bool): whether we should navigate to record
         """
-        self.navigate_to_entity('user')
-        self.navigate_to_record(user)
+
+        if navigate:
+            self.navigate_to_entity(entity)
+            self.navigate_to_record(pkey)
 
         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 found on current page'
+
+        for btn in rm_btns:
+            btn.click()
+
         self.facet_button_click('save')
 
+    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)
+
     def run_cmd_on_ui_host(self, cmd):
         """
         Run "shell" command on the UI system using "admin" user's passwd from
@@ -2070,3 +2180,22 @@ 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 value not 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 3e7f180d18e8a986cfb8f4ae61b1b3e60dc041fb Mon Sep 17 00:00:00 2001
From: Michal Reznik <mrez...@redhat.com>
Date: Wed, 25 Apr 2018 10:51:19 +0200
Subject: [PATCH 3/3] ui_tests: introduce new test_misc cases file

By this commit we introduce new test_misc cases file to
test various miscellaneous cases that do not fit to other suites.

In this cases that "version" is present in profile`s "about".

https://pagure.io/freeipa/issue/7507
---
 ipatests/test_webui/test_misc_cases.py | 28 ++++++++++++++++++++++++++++
 1 file changed, 28 insertions(+)
 create mode 100644 ipatests/test_webui/test_misc_cases.py

diff --git a/ipatests/test_webui/test_misc_cases.py b/ipatests/test_webui/test_misc_cases.py
new file mode 100644
index 0000000000..5f7ffb54ee
--- /dev/null
+++ b/ipatests/test_webui/test_misc_cases.py
@@ -0,0 +1,28 @@
+#
+# Copyright (C) 2018  FreeIPA Contributors see COPYING for license
+#
+
+"""
+Place for various miscellaneous test cases that do not fit to other suites
+"""
+
+from ipatests.test_webui.ui_driver import UI_driver
+from ipatests.test_webui.ui_driver import screenshot
+import pytest
+import re
+
+
+@pytest.mark.tier1
+class TestMiscCases(UI_driver):
+
+    @screenshot
+    def test_version_present(self):
+
+        self.init_app()
+
+        self.profile_menu_action('about')
+
+        about_text = self.get_text('div[data-name="version_dialog"] p')
+        ver_re = re.compile('version: .*')
+        assert re.search(ver_re, about_text), 'Version not found'
+        self.dialog_button_click('ok')
_______________________________________________
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