diff --git a/ipatests/test_webui/data_loginscreen.py b/ipatests/test_webui/data_loginscreen.py new file mode 100644 index 000000000..62063a606 --- /dev/null +++ b/ipatests/test_webui/data_loginscreen.py @@ -0,0 +1,161 @@ +# +# Copyright (C) 2018 FreeIPA Contributors see COPYING for license +# + +ENTITY = 'user' + +PKEY = 'itest-user' +PASSWD_ITEST_USER = '12345678' +PASSWD_ITEST_USER_NEW = '87654321' + +# used for add/delete fixture test user +DATA_ITEST_USER = { + 'pkey': PKEY, + 'add': [ + ('textbox', 'uid', PKEY), + ('textbox', 'givenname', 'itest-user-name'), + ('textbox', 'sn', 'itest-user-surname'), + ('password', 'userpassword', PASSWD_ITEST_USER), + ('password', 'userpassword2', PASSWD_ITEST_USER), + ] +} + +# used for checking login form after click Cancel on 'reset' view +FILLED_LOGIN_FORM = { + # structure of rows + # label_name, label_text, + # required, editable, + # input_type, input_name, + # input_text, placeholder + 'rows': [ + ('username', 'Username', True, True, 'text', 'username', + PKEY, 'Username'), + ('password', 'Password', True, True, 'password', 'password', + PASSWD_ITEST_USER, 'Password or Password+One-Time-Password'), + ], + # structure of buttons + # button_name, button_title + 'buttons': [ + ('cert_auth', 'Log in using personal certificate'), + ('sync', 'Sync OTP Token'), + ('login', 'Log in'), + ], + 'required_msg': [ + ('Username: Required field',), + ('Password: Required field',), + ], +} + +# used for checking 'reset_and_login' view +RESET_AND_LOGIN_FORM = { + # structure of rows + # label_name, label_text, + # required, editable, + # input_type, input_name, + # input_text, placeholder + 'rows': [ + ('username_r', 'Username', False, False, None, 'username_r', + PKEY, None), + ('current_password', 'Current Password', False, True, 'password', + 'current_password', '', 'Current Password'), + ('otp', 'OTP', False, True, 'password', 'otp', '', + 'One-Time-Password'), + ('new_password', 'New Password', True, True, 'password', + 'new_password', '', 'New Password'), + ('verify_password', 'Verify Password', True, True, 'password', + 'verify_password', '', 'New Password'), + ], + # structure of buttons + # button_name, button_title + 'buttons': [ + ('cancel', 'Cancel'), + ('reset_and_login', 'Reset Password and Log in'), + ], + 'required_msg': [ + ('New Password: Required field',), + ('Verify Password: Required field',), + ], +} + +# used for checking 'reset' view +RESET_PASSWORD_FORM = { + # structure of rows + # label_name, label_text, + # required, editable, + # input_type, input_name, + # input_text, placeholder + 'rows': [ + ('username', 'Username', True, True, 'text', 'username', '', + 'Username'), + ('current_password', 'Current Password', True, True, 'password', + 'current_password', '', 'Current Password'), + ('otp', 'OTP', False, True, 'password', 'otp', '', + 'One-Time-Password'), + ('new_password', 'New Password', True, True, 'password', + 'new_password', '', 'New Password'), + ('verify_password', 'Verify Password', True, True, 'password', + 'verify_password', '', 'New Password'), + ], + # structure of buttons + # button_name, button_title + 'buttons': [ + ('reset', 'Reset Password'), + ], + 'required_msg': [ + ('Username: Required field',), + ('Current Password: Required field',), + ('New Password: Required field',), + ('Verify Password: Required field',), + ], +} + + +# used for checking empty 'login' view +EMPTY_LOGIN_FORM = { + # structure of rows + # label_name, label_text, + # required, editable, + # input_type, input_name, + # input_text, placeholder + 'rows': [ + ('username', 'Username', False, True, 'text', 'username', '', + 'Username'), + ('password', 'Password', False, True, 'password', 'password', '', + 'Password or Password+One-Time-Password'), + ], + # structure of buttons + # button_name, button_title + 'buttons': [ + ('cert_auth', 'Log in using personal certificate'), + ('sync', 'Sync OTP Token'), + ('login', 'Log in'), + ], + 'required_msg': [ + ('Authentication with Kerberos failed',), + ], +} + +# used for checking 'login' view +LOGIN_FORM = { + # structure of rows + # label_name, label_text, + # required, editable, + # input_type, input_name, + # input_text, placeholder + 'rows': [ + ('username', 'Username', True, True, 'text', 'username', PKEY, + 'Username'), + ('password', 'Password', True, True, 'password', 'password', '', + 'Password or Password+One-Time-Password'), + ], + # structure of buttons + # button_name, button_title + 'buttons': [ + ('cert_auth', 'Log in using personal certificate'), + ('sync', 'Sync OTP Token'), + ('login', 'Log in'), + ], + 'required_msg': [ + ('Password: Required field',), + ], +} diff --git a/ipatests/test_webui/test_loginscreen.py b/ipatests/test_webui/test_loginscreen.py new file mode 100644 index 000000000..ef4a26ad5 --- /dev/null +++ b/ipatests/test_webui/test_loginscreen.py @@ -0,0 +1,401 @@ +# +# Copyright (C) 2018 FreeIPA Contributors see COPYING for license +# + +""" +Test LoginScreen widget and all it's views +""" + +from ipatests.test_webui.ui_driver import UI_driver +from ipatests.test_webui.ui_driver import screenshot +import ipatests.test_webui.data_loginscreen as loginscreen + +try: + from selenium.webdriver.common.by import By + from selenium.webdriver.common.keys import Keys + from selenium.webdriver.support.wait import WebDriverWait +except ImportError: + pass + +import pytest +from six.moves import urllib + + +@pytest.mark.tier1 +class TestLoginScreen(UI_driver): + + def setup(self, *args, **kwargs): + super(TestLoginScreen, self).setup(*args, **kwargs) + self.init_app() + self.add_test_user() + self.logout() + + def teardown(self, *args, **kwargs): + # log out first + if (self.logged_in()): + self.logout() + else: + self.load_url(self.get_base_url()) + # log in as administrator + self.login() + self.delete_test_user() + super(TestLoginScreen, self).teardown(*args, **kwargs) + + def delete_test_user(self): + """ + Delete user for tests + """ + # User is not logged in + assert self.logged_in() + self.navigate_to_entity(loginscreen.ENTITY) + self.delete_record(loginscreen.PKEY) + + def add_test_user(self): + """ + Add user for tests + """ + # User is not logged in + assert self.logged_in() + self.add_record(loginscreen.ENTITY, loginscreen.DATA_ITEST_USER, + navigate=False) + + def assert_notification(self, type='success', assert_text=None, + link_text=None, link_url=None): + """ + Assert whether we have a notification of particular type + """ + + notification_type = 'div.validation-summary .alert-{}'.format(type) + # wait for a half sec for notification to appear + self.wait(0.5) + is_present = self.find(notification_type, By.CSS_SELECTOR) + # Notification not present + assert is_present + if assert_text: + assert assert_text in is_present.text + + if link_text and link_url: + link = self.find_xelement(".//a", is_present) + # Text on link placed on validation widget + assert link_text == link.text + # URL of link placed on validation widget + assert link_url == link.get_attribute('href') + + def find_xelement(self, expression, parent, strict=True): + """ + Find element by xpath related to a given parent + """ + return self.find(expression, By.XPATH, parent, many=False, + strict=strict) + + def find_xelements(self, expression, parent, strict=True): + """ + Find elements by xpath related to the given parent + """ + return self.find(expression, By.XPATH, parent, many=True, + strict=strict) + + def button_click_on_login_screen(self, name): + """ + Find a button with the given name on LoginScreen widget and + then click on + """ + login_screen = self.get_login_screen() + button = self.find_xelement(".//button[@name='{}']".format(name), + login_screen) + assert button.is_displayed() + button.click() + + def get_input_field(self, name, parent): + """ + Find a input field with the given name and parent + """ + return self.find_xelement( + ".//input[@name='{}']".format(name), + parent + ) + + def load_url(self, url): + """ + Navigate to Web page and wait for loading of all dependencies. + """ + self.driver.get(url) + runner = self + WebDriverWait(self.driver, 10).until( + lambda d: runner.files_loaded() + ) + + def relogin_with_new_password(self): + """ + Log out and then log in using a new password. + It is need to check a new password + """ + if (self.logged_in()): + self.logout() + else: + self.load_url(self.get_base_url()) + self.login(loginscreen.PKEY, loginscreen.PASSWD_ITEST_USER_NEW) + # User is not logged in + assert self.logged_in() + + def reset_password(self, username=None, current_password=None, + new_password=None, link_text=None, link_url=None): + """ + Reset password with the given one + """ + login_screen = self.get_login_screen() + + if username is not None: + username_field = self.get_input_field('username', login_screen) + cur_pass_field = self.get_input_field('current_password', login_screen) + new_pass_field = self.get_input_field('new_password', login_screen) + verify_pass_field = self.get_input_field('verify_password', + login_screen) + if username is not None: + username_field.send_keys(username) + cur_pass_field.send_keys(current_password) + new_pass_field.send_keys(new_password) + verify_pass_field.send_keys(new_password) + verify_pass_field.send_keys(Keys.RETURN) + self.wait() + self.assert_notification(assert_text='Password change complete', + link_text=link_text, link_url=link_url) + + def get_data_from_form_row(self, form_row): + """ + Parse data from the form record to a comparable structure + """ + result = [] + label = self.find_xelement(".//label", form_row) + assert label.is_displayed() + result.append(label.get_attribute('name')) + result.append(label.text) + req = self.find_xelement("./../..", label) + result.append(self.has_class(req, 'required')) + + field = self.find_xelement(".//input", form_row) + # not editable field + editable = field.is_displayed() + if not editable: + field = self.find_xelement(".//p", form_row) + + result.append(editable) + assert field.is_displayed() + result.append(field.get_attribute('type')) + result.append(field.get_attribute('name')) + if not editable: + result.append(field.text) + else: + result.append(field.get_attribute('value')) + result.append(field.get_attribute('placeholder')) + + return tuple(result) + + def get_data_from_button(self, button): + """ + Parse data from the button to a comparable structure + """ + result = [] + result.append(button.get_attribute('name')) + result.append(button.get_attribute('title')) + + return tuple(result) + + def assert_form_equals(self, actual_form, expected_form): + """ + Compare two forms + """ + assert len(actual_form) == len(expected_form) + for act_row, exp_row in zip(actual_form, expected_form): + # structure of rows + # label_name, label_text, + # required, editable, + # input_type, input_name, + # input_text, placeholder + assert self.get_data_from_form_row(act_row) == exp_row + + def assert_buttons_equal(self, actual_buttons, expected_buttons): + """ + Compare button sets + """ + assert len(actual_buttons) == len(expected_buttons) + for act_button, exp_button in zip(actual_buttons, expected_buttons): + assert self.get_data_from_button(act_button) == exp_button + + def assert_validations_equal(self, actual_alerts, expected_alerts): + """ + Compare validation sets + """ + assert len(actual_alerts) == len(expected_alerts) + for act_alert, exp_alert in zip(actual_alerts, expected_alerts): + assert (act_alert.text,) == exp_alert + + def has_validation(self, parent): + return self.find_xelement(".//div[@name='validation']", parent, + strict=False) + + def check_elements_of_form(self, form_data): + + login_screen = self.get_login_screen() + form = self.find_xelement(".//div[@class='form-horizontal']", + login_screen) + # rows + form_rows = self.find_xelements( + ".//div[contains(@class, 'form-group')]", form + ) + + form_rows = [el for el in form_rows if el.is_displayed() and not + self.has_validation(el)] + self.assert_form_equals(form_rows, form_data['rows']) + + # buttons + buttons = self.find_xelements(".//button", login_screen) + buttons = [el for el in buttons if el.is_displayed()] + self.assert_buttons_equal(buttons, form_data['buttons']) + + def check_alerts(self, form_data): + login_screen = self.get_login_screen() + + # Push the the most rigth button to see the Required fields + # it should be either 'Reset' or 'Reset and Login' or 'Login' button + self.button_click_on_login_screen(form_data['buttons'][-1][0]) + + alerts = self.find_xelements( + ".//*[@data-name][contains(@class, 'alert-danger')]", login_screen + ) + required_msgs = form_data['required_msg'] + self.assert_validations_equal(alerts, required_msgs) + + def load_reset_and_login_view(self): + + self.load() + assert self.login_screen_visible() + username = loginscreen.PKEY + current_password = loginscreen.PASSWD_ITEST_USER + + login_screen = self.get_login_screen() + username_field = self.get_input_field('username', login_screen) + cur_pass_field = self.get_input_field('password', login_screen) + username_field.send_keys(username) + cur_pass_field.send_keys(current_password) + cur_pass_field.send_keys(Keys.RETURN) + self.wait() + self.assert_notification( + type='info', + assert_text=( + 'Your password has expired. Please enter a new password.' + ) + ) + + def check_cancel(self): + """ + Check 'login' view after a cancel of password reset + """ + self.button_click_on_login_screen('cancel') + self.check_elements_of_form(loginscreen.FILLED_LOGIN_FORM) + + @screenshot + def test_reset_password_view(self): + + self.load_url('/'.join((self.get_base_url(), 'reset_password.html'))) + assert self.login_screen_visible() + + self.check_elements_of_form(loginscreen.RESET_PASSWORD_FORM) + self.check_alerts(loginscreen.RESET_PASSWORD_FORM) + + username = loginscreen.PKEY + current_password = loginscreen.PASSWD_ITEST_USER + new_password = loginscreen.PASSWD_ITEST_USER_NEW + self.reset_password(username, current_password, new_password) + self.relogin_with_new_password() + + @screenshot + def test_reset_password_view_with_redirect(self): + + redir_url = self.get_base_url().lower() + encoded_redir_url = urllib.parse.urlencode({'url': redir_url}) + target_url = '/'.join((self.get_base_url(), 'reset_password.html?{}')) + self.load_url(target_url.format(encoded_redir_url)) + assert self.login_screen_visible() + + self.check_elements_of_form(loginscreen.RESET_PASSWORD_FORM) + self.check_alerts(loginscreen.RESET_PASSWORD_FORM) + + username = loginscreen.PKEY + current_password = loginscreen.PASSWD_ITEST_USER + new_password = loginscreen.PASSWD_ITEST_USER_NEW + self.reset_password(username, current_password, new_password, + link_text='Continue to next page', + link_url=redir_url, + ) + self.relogin_with_new_password() + + @screenshot + def test_reset_password_view_with_delayed_redirect(self): + + redir_url = self.get_base_url().lower() + '/' + encoded_redir_url = urllib.parse.urlencode( + {'url': redir_url, 'delay': 5} + ) + target_url = '/'.join((self.get_base_url(), 'reset_password.html?{}')) + self.load_url(target_url.format(encoded_redir_url)) + assert self.login_screen_visible() + + self.check_elements_of_form(loginscreen.RESET_PASSWORD_FORM) + self.check_alerts(loginscreen.RESET_PASSWORD_FORM) + username = loginscreen.PKEY + current_password = loginscreen.PASSWD_ITEST_USER + new_password = loginscreen.PASSWD_ITEST_USER_NEW + self.reset_password(username, current_password, new_password, + link_text='Continue to next page', + link_url=redir_url, + ) + self.assert_notification(type='info', + assert_text='You will be redirected in ') + self.wait(3) + # check url after start delay timer, but before end + assert self.driver.current_url != redir_url + self.wait(5) + assert self.driver.current_url == redir_url + self.relogin_with_new_password() + + @screenshot + def test_reset_password_and_login_view(self): + + self.load_reset_and_login_view() + self.check_elements_of_form(loginscreen.RESET_AND_LOGIN_FORM) + self.check_alerts(loginscreen.RESET_AND_LOGIN_FORM) + + # check click on 'Cancel' button + self.check_cancel() + self.button_click_on_login_screen('login') + + # check if user is not logged + assert not self.logged_in() + current_password = loginscreen.PASSWD_ITEST_USER + new_password = loginscreen.PASSWD_ITEST_USER_NEW + # username is already here, do not fill up + self.reset_password(current_password=current_password, + new_password=new_password) + # waiting for auto login process + self.wait(5) + assert self.logged_in() + # check again if new password is valid + self.relogin_with_new_password() + + @screenshot + def test_login_view(self): + + self.load() + + # check empty 'login' view + self.check_elements_of_form(loginscreen.EMPTY_LOGIN_FORM) + self.check_alerts(loginscreen.EMPTY_LOGIN_FORM) + + # check non empty 'login' view + login_screen = self.get_login_screen() + username_field = self.get_input_field('username', login_screen) + username_field.send_keys(loginscreen.PKEY) + self.wait(0.5) + self.check_elements_of_form(loginscreen.LOGIN_FORM) + self.check_alerts(loginscreen.LOGIN_FORM)