pages/desktop/frontend/login.py (220 lines of code) (raw):

import os import time import requests import pyotp from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from pages.desktop.base import Base from scripts import reusables class Login(Base): """The following variables need to be set as Environment Variables when running tests locally. These variables are also set in CircleCI's Project level Environment Variables and are picked up at runtime""" # 1. user that performs normal operations on the site, like writing add-on reviews REGULAR_USER_EMAIL = os.environ.get("REGULAR_USER_EMAIL") REGULAR_USER_PASSWORD = os.environ.get("REGULAR_USER_PASSWORD") # 2. user with elevated permissions that can perform special actions on the site ADMIN_USER_EMAIL = os.environ.get("ADMIN_USER_EMAIL") ADMIN_USER_PASSWORD = os.environ.get("ADMIN_USER_PASSWORD") # 3. user who has published add-ons on AMO DEVELOPER_EMAIL = os.environ.get("DEVELOPER_EMAIL") DEVELOPER_PASSWORD = os.environ.get("DEVELOPER_PASSWORD") # 4. user who re-creates accounts on AMO after having deleted them previously REUSABLE_USER_EMAIL = os.environ.get("REUSABLE_USER_EMAIL") REUSABLE_USER_PASSWORD = os.environ.get("REUSABLE_USER_PASSWORD") # 5. user used for the ratings tests RATING_USER_EMAIL = os.environ.get("RATING_USER_EMAIL") RATING_USER_PASSWORD = os.environ.get("RATING_USER_PASSWORD") # 6. user used for collections tests COLLECTION_USER_EMAIL = os.environ.get("COLLECTION_USER_EMAIL") COLLECTION_USER_PASSWORD = os.environ.get("COLLECTION_USER_PASSWORD") # 7. user used for add-on submissions SUBMISSIONS_USER_EMAIL = os.environ.get("SUBMISSIONS_USER_EMAIL") SUBMISSIONS_USER_PASSWORD = os.environ.get("SUBMISSIONS_USER_PASSWORD") # 8. user used in API tests API_USER_EMAIL = os.environ.get("API_USER_EMAIL") API_USER_PASSWORD = os.environ.get("API_USER_PASSWORD") # 9. user with a mozilla account that has specific submission permissions STAFF_USER_EMAIL = os.environ.get("STAFF_USER_EMAIL") STAFF_USER_PASSWORD = os.environ.get("STAFF_USER_PASSWORD") # 10. account added to the list of banned user emails for rating and addon submissions RESTRICTED_USER_EMAIL = os.environ.get("RESTRICTED_USER_EMAIL") RESTRICTED_USER_PASSWORD = os.environ.get("RESTRICTED_USER_PASSWORD") # 11. account for reviewer tools added in order to help with release and coverage tests(doesn't have full access) REVIEWER_TOOLS_USER_EMAIL = os.environ.get("REVIEWER_TOOLS_USER_EMAIL") REVIEWER_TOOLS_USER_PASSWORD = os.environ.get("REVIEWER_TOOLS_USER_PASSWORD") # KEYS FOR AUTHENTICATOR DEV DEVELOPER_USER_KEY_DEV = os.environ.get("DEVELOPER_USER_KEY_DEV") RATING_USER_KEY_DEV = os.environ.get("RATING_USER_KEY_DEV") SUBMISSIONS_USER_KEY_DEV = os.environ.get("SUBMISSIONS_USER_KEY_DEV") API_USER_KEY_DEV = os.environ.get("API_USER_KEY_DEV") STAFF_USER_KEY_DEV = os.environ.get("STAFF_USER_KEY_DEV") # KEYS FOR AUTHENTICATOR STAGE DEVELOPER_USER_KEY_STAGE = os.environ.get("DEVELOPER_USER_KEY_STAGE") RATING_USER_KEY_STAGE = os.environ.get("RATING_USER_KEY_STAGE") SUBMISSIONS_USER_KEY_STAGE = os.environ.get("SUBMISSIONS_USER_KEY_STAGE") API_USER_KEY_STAGE = os.environ.get("API_USER_KEY_STAGE") STAFF_USER_KEY_STAGE = os.environ.get("STAFF_USER_KEY_STAGE") REVIEWER_TOOLS_USER_KEY = os.environ.get("REVIEWER_TOOLS_USER_KEY") _email_locator = (By.NAME, "email") _continue_locator = (By.CSS_SELECTOR, ".button-row button") _password_login = (By.CSS_SELECTOR, ".pb-1") _password_locator = (By.XPATH, "//input[@data-testid='new-password-input-field']") _login_btn_locator = (By.CSS_SELECTOR, "button.cta-primary.cta-xl") _repeat_password_locator = (By.CSS_SELECTOR, "div.relative:nth-child(3) > div:nth-child(1) > label:nth-child(1) > span:nth-child(1) > input:nth-child(2)") _age_locator = (By.CSS_SELECTOR, "label.flex:nth-child(4) > span:nth-child(1) > input") _code_input_locator = (By.CSS_SELECTOR, ".pb-1") _login_card_header_locator = (By.CSS_SELECTOR, ".card-header") _2fa_input_locator = (By.CSS_SELECTOR, ".pb-1") _confirm_2fa_button_locator = (By.CSS_SELECTOR, ".cta-primary") _error_2fa_code_locator = (By.CSS_SELECTOR, ".text-xs") def account(self, user): if user == "reusable_user": self.fxa_login(self.REUSABLE_USER_EMAIL, self.REUSABLE_USER_PASSWORD, "") elif user == "admin": self.fxa_login(self.ADMIN_USER_EMAIL, self.ADMIN_USER_PASSWORD, "") elif user == "developer": if "dev.allizom" not in self.base_url: self.fxa_login( self.DEVELOPER_EMAIL, self.DEVELOPER_PASSWORD, self.DEVELOPER_USER_KEY_STAGE, ) else: self.fxa_login( self.DEVELOPER_EMAIL, self.DEVELOPER_PASSWORD, self.DEVELOPER_USER_KEY_DEV, ) elif user == "rating_user": if "dev.allizom" not in self.base_url: self.fxa_login( self.RATING_USER_EMAIL, self.RATING_USER_PASSWORD, self.RATING_USER_KEY_STAGE, ) else: self.fxa_login( self.RATING_USER_EMAIL, self.RATING_USER_PASSWORD, self.RATING_USER_KEY_DEV, ) elif user == "collection_user": self.fxa_login( self.COLLECTION_USER_EMAIL, self.COLLECTION_USER_PASSWORD, "" ) elif user == "submissions_user": if "dev.allizom" not in self.base_url: self.fxa_login( self.SUBMISSIONS_USER_EMAIL, self.SUBMISSIONS_USER_PASSWORD, self.SUBMISSIONS_USER_KEY_STAGE, ) else: self.fxa_login( self.SUBMISSIONS_USER_EMAIL, self.SUBMISSIONS_USER_PASSWORD, self.SUBMISSIONS_USER_KEY_DEV, ) elif user == "api_user": if "dev.allizom" not in self.base_url: self.fxa_login( self.API_USER_EMAIL, self.API_USER_PASSWORD, self.API_USER_KEY_STAGE ) else: self.fxa_login( self.API_USER_EMAIL, self.API_USER_PASSWORD, self.API_USER_KEY_DEV ) elif user == "staff_user": if "dev.allizom" not in self.base_url: self.fxa_login( self.STAFF_USER_EMAIL, self.STAFF_USER_PASSWORD, self.STAFF_USER_KEY_STAGE, ) else: self.fxa_login( self.STAFF_USER_EMAIL, self.STAFF_USER_PASSWORD, self.STAFF_USER_KEY_DEV, ) elif user == "restricted_user": self.fxa_login( self.RESTRICTED_USER_EMAIL, self.RESTRICTED_USER_PASSWORD, "" ) elif user == "reviewer_user": self.fxa_login( self.REVIEWER_TOOLS_USER_EMAIL, self.REVIEWER_TOOLS_USER_PASSWORD, self.REVIEWER_TOOLS_USER_KEY ) else: self.fxa_login(self.REGULAR_USER_EMAIL, self.REGULAR_USER_PASSWORD, "") def fxa_login(self, email, password, key): self.find_element(*self._email_locator).send_keys(email) # sometimes, the login function fails on the 'continue_btn.click()' event with a TimeoutException # triggered by the built'in timeout of the 'click()' method; # however, the screenshot captured by the html report at test fail time shows that the click occurred # since the expected page has been loaded; # this seems to be a reoccurring issue in geckodriver as explained in # https://github.com/mozilla/geckodriver/issues/1608; # here, I'm capturing that TimeoutException and trying to push the script to continue to the next steps. try: continue_btn = self.wait.until( EC.element_to_be_clickable((By.ID, "submit-btn")) ) continue_btn.click() except TimeoutException as error: print(error.msg) pass print('The "click continue button" event occurred.') self.wait.until( EC.element_to_be_clickable(self._password_login), message=f"Password input field not displayed; " # f"FxA card header was {self.find_element(*self._login_card_header_locator).text}", ) # print( # f'The script should be on the password input screen here. We should see "Sign in" in the header.' # f' The card header title is "{self.find_element(*self._login_card_header_locator).text}"' # ) self.find_element(*self._password_login).send_keys(password) # waits for the password to be filled in self.wait.until( EC.invisibility_of_element_located((By.CSS_SELECTOR, ".password.empty")), message="There was no input added in the password field", ) self.find_element(*self._login_btn_locator).click() # logic for 2fa enabled accounts if key != "": self.wait.until(EC.url_contains("signin_totp_code")) self.wait.until(EC.visibility_of_element_located(self._2fa_input_locator)) time.sleep(30) totp = pyotp.TOTP(key) self.find_element(*self._2fa_input_locator).send_keys(totp.now()) self.find_element(*self._confirm_2fa_button_locator).click() time.sleep(5) for max_retries in range(0, 2): if self.is_element_displayed(*self._error_2fa_code_locator): time.sleep(500) totp = pyotp.TOTP(key) self.find_element(*self._2fa_input_locator).clear() self.find_element(*self._2fa_input_locator).send_keys(totp.now()) self.find_element(*self._confirm_2fa_button_locator).click() else: break # wait for transition between FxA page and AMO self.wait.until( EC.url_contains("addons"), message=f"AMO could not be loaded in {self.driver.current_url}. " f"Response status code was {requests.head(self.driver.current_url).status_code}", ) def fxa_register(self): email = f"{reusables.get_random_string(10)}@restmail.net" password = reusables.get_random_string(12) self.find_element(*self._email_locator).send_keys(email) # catching the geckodriver click() issue, in cae it happens here # issue - https://github.com/mozilla/geckodriver/issues/1608 try: continue_btn = self.wait.until( EC.element_to_be_clickable((By.ID, "submit-btn")) ) continue_btn.click() except TimeoutException as error: print(error.msg) pass # verify that the fxa register form was opened time.sleep(5) self.wait.until( EC.element_to_be_clickable(self._password_locator), message=f"Password input field not displayed; " f"FxA card header was {self.find_element(*self._login_card_header_locator).text}", ) self.find_element(*self._password_locator).send_keys(password) self.find_element(*self._repeat_password_locator).send_keys(password) self.find_element(*self._age_locator).send_keys(23) self.find_element(*self._login_btn_locator).click() # sleep to allow FxA to process the request and communicate with the email client time.sleep(10) verification_code = self.get_verification_code(email) self.find_element(*self._code_input_locator).send_keys(verification_code) self.find_element(*self._login_btn_locator).click() def get_verification_code(self, mail): request = requests.get(f"https://restmail.net/mail/{mail}", timeout=10) response = request.json() # creating a timed loop to address a possible communication delay between # FxA and restmail; this loop polls the endpoint for 20s to await a response # and exits if there was no response received in the given amount of time timeout_start = time.time() while time.time() < timeout_start + 20: if response: verification_code = [ key["headers"]["x-verify-short-code"] for key in response ] return verification_code elif not response: requests.get(f"https://restmail.net/mail/{mail}", timeout=10) print("Restmail did not receive an email from FxA") return self