From 8ad07a2433da637192ecf245e6a6abb12ea8dba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gediminas=20Al=C5=A1auskas?= Date: Mon, 15 Apr 2024 13:53:59 +0300 Subject: [PATCH] Automate login callback test using selenium --- test/qa/lib/login.py | 82 ++++++++++++++++++++++++ test/qa/test_login_nordaccount.py | 103 ++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 test/qa/test_login_nordaccount.py diff --git a/test/qa/lib/login.py b/test/qa/lib/login.py index 4098ef5ee..b645221f7 100644 --- a/test/qa/lib/login.py +++ b/test/qa/lib/login.py @@ -2,9 +2,91 @@ import os import sh +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.support.expected_conditions import presence_of_element_located +from selenium.webdriver.support.ui import WebDriverWait from . import logging, ssh +# Path where we save screenshots from browser, of Selenium tests, if they fail +BROWSER_LOGS_PATH = f"{os.environ['WORKDIR']}/dist/logs/" #browser_tests/" + +LOGIN_MSG_SUCCESS = "Welcome to NordVPN! You can now connect to VPN by using 'nordvpn connect'." +LOGOUT_MSG_SUCCESS = "You are logged out." + +# environment variable, which we will use to specify location to temporary folder for Firefox +ENV_FF_TMP = "TMPDIR" + +# location to store temporary Firefox files, in case Firefox was installed from Snap (prevents profile error) +SNAP_FF_TMP_DIR_PATH = os.path.expanduser("~") + "/ff_tmp" + +# XPath used in Selenium tests, to determine locations of elements on NordAccount +NA_USERNAME_PAGE_TEXTBOX_XPATH = '/html/body/div/div/div[1]/main/form/fieldset/div/span/input' +NA_USERNAME_PAGE_BUTTON_XPATH = '/html/body/div/div/div[1]/main/form/fieldset/button' + +NA_PASSWORD_PAGE_TEXTBOX_XPATH = '/html/body/div/div/div[1]/main/form/fieldset/div[3]/span/input' +NA_PASSWORD_PAGE_BUTTON_XPATH = NA_USERNAME_PAGE_BUTTON_XPATH + +NA_CONTINUE_PAGE_LINK_BUTTON = '/html/body/div/div/div[1]/main/div/a' + +# used with Selenium login tests +LOGIN_FLAG = ["", "--nordaccount"] + + +class SeleniumBrowser: + def __init__(self, preferences:list=None): + self.options = webdriver.FirefoxOptions() + self.options.binary_location = str(sh.which("firefox")) + self.options.add_argument('--headless') + self.options.add_argument('--no-sandbox') + + if preferences is not None: + for preference, value in preferences: + self.options.set_preference(preference, value) + + if os.path.exists("/snap/firefox") and os.environ.get(ENV_FF_TMP) is None: + os.mkdir(SNAP_FF_TMP_DIR_PATH) + os.environ[ENV_FF_TMP] = SNAP_FF_TMP_DIR_PATH + + service = webdriver.FirefoxService(executable_path="/usr/bin/geckodriver") #, log_output=BROWSER_LOGS_PATH + "geckodriver.log") + self.browser = webdriver.Firefox(options=self.options, service=service) + + def browser_get(self) -> webdriver.Firefox: + return self.browser + + def browser_kill(self) -> None: + """ Quits browser, and deletes temporary folder created for Firefox. """ + self.browser.quit() + + # Cleanup, if we were working with Firefox from snap + if os.environ.get(ENV_FF_TMP) is not None: + os.environ.pop(ENV_FF_TMP) + os.removedirs(SNAP_FF_TMP_DIR_PATH) + + def browser_element_interact(self, xpath: str, write:str=None, return_attribute:str=None) -> None | str: + """ + Clicks element on website, specified by `xpath`. + + If `write` parameter value is set, also writes the set value to the element. + + If `return_attribute` parameter value is set, also returns specified attribute value of element. + """ + + # Defines how long will we wait for elements, that we are looking for to appear in pages + wait = WebDriverWait(self.browser, 10) + + website_element = wait.until(presence_of_element_located((By.XPATH, xpath))) + website_element.click() + + if write is not None: + website_element.send_keys(write) + + if return_attribute is not None: + return website_element.get_attribute(return_attribute) + else: + return None + def get_default_credentials(): """Returns tuple[username,token].""" diff --git a/test/qa/test_login_nordaccount.py b/test/qa/test_login_nordaccount.py new file mode 100644 index 000000000..649b4b728 --- /dev/null +++ b/test/qa/test_login_nordaccount.py @@ -0,0 +1,103 @@ +import os + +import pytest +import sh +import timeout_decorator + +import lib +from lib import ( + daemon, + info, + logging, + login, +) + + +def setup_function(function): # noqa: ARG001 + daemon.start() + + logging.log() + + +def teardown_function(function): # noqa: ARG001 + logging.log(data=info.collect()) + logging.log() + + sh.nordvpn.set.defaults() + daemon.stop() + + +@pytest.mark.skip("Does not work on Docker") +@pytest.mark.parametrize("login_flag", login.LOGIN_FLAG) +@timeout_decorator.timeout(60) +def test_selenium_login(login_flag): + preferences = [ + ["network.protocol-handler.expose.nordvpn", True], + ["network.protocol-handler.external.nordvpn", True], + ["network.protocol-handler.app.nordvpn", "/usr/bin/nordvpn"] + ] + + selenium = login.SeleniumBrowser(preferences) + browser = selenium.browser_get() + + with lib.Defer(selenium.browser_kill): + # Get login link from NordVPN app, trim all spaces & chars after link itself + login_link = sh.nordvpn.login(login_flag, _tty_out=False).strip().split(": ")[1] + + # Open login link from NordVPN app + browser.get(login_link) + + # User credentials, that we will use in order to log in to NordAccount + user_info = os.environ.get("DEFAULT_LOGIN_USERNAME") + ":" + os.environ.get("DEFAULT_LOGIN_PASSWORD") + + try: + # Username page + selenium.browser_element_interact(login.NA_USERNAME_PAGE_TEXTBOX_XPATH, user_info.split(':')[0]) + selenium.browser_element_interact(login.NA_USERNAME_PAGE_BUTTON_XPATH) + + # Password page + selenium.browser_element_interact(login.NA_PASSWORD_PAGE_TEXTBOX_XPATH, user_info.split(':')[1]) + selenium.browser_element_interact(login.NA_PASSWORD_PAGE_BUTTON_XPATH) + + # Continue to app page + selenium.browser_element_interact(login.NA_CONTINUE_PAGE_LINK_BUTTON) + except: # noqa: E722 + browser.save_screenshot(login.BROWSER_LOGS_PATH + "Screenshot.png") + pytest.fail() + + assert login.LOGOUT_MSG_SUCCESS in sh.nordvpn.logout() + + +@pytest.mark.parametrize("login_flag", login.LOGIN_FLAG) +@timeout_decorator.timeout(60) +def test_selenium_login_callback(login_flag): + selenium = login.SeleniumBrowser() + browser = selenium.browser_get() + + with lib.Defer(selenium.browser_kill): + # Get login link from NordVPN app, trim all spaces & chars after link itself + login_link = sh.nordvpn.login(login_flag, _tty_out=False).strip().split(": ")[1] + + # Open login link from NordVPN app + browser.get(login_link) + + # User credentials, that we will use in order to log in to NordAccount + user_info = os.environ.get("DEFAULT_LOGIN_USERNAME") + ":" + os.environ.get("DEFAULT_LOGIN_PASSWORD") + + try: + # Username page + selenium.browser_element_interact(login.NA_USERNAME_PAGE_TEXTBOX_XPATH, user_info.split(':')[0]) + selenium.browser_element_interact(login.NA_USERNAME_PAGE_BUTTON_XPATH) + + # Password page + selenium.browser_element_interact(login.NA_PASSWORD_PAGE_TEXTBOX_XPATH, user_info.split(':')[1]) + selenium.browser_element_interact(login.NA_PASSWORD_PAGE_BUTTON_XPATH) + + # Continue to app page + # preferences not set in constructor, so when we click link it does not redirect us to app. + callback_link = selenium.browser_element_interact(login.NA_CONTINUE_PAGE_LINK_BUTTON, return_attribute='href') + except: # noqa: E722 + browser.save_screenshot(login.BROWSER_LOGS_PATH + "Screenshot.png") + pytest.fail() + + assert login.LOGIN_MSG_SUCCESS in sh.nordvpn.login("--callback", callback_link)