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