diff --git a/pyproject.toml b/pyproject.toml index 0fafd65f43..3e8292a1aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ test = [ "pytest-django", "pytest-mock", "pytest-playwright", # only for writing tests. run tests using playwright Docker Compose service + "pyotp", "pytest-socket", ] diff --git a/tests/playwright/test_healthcheck.py b/tests/playwright/healthcheck_tests/test_healthcheck.py similarity index 69% rename from tests/playwright/test_healthcheck.py rename to tests/playwright/healthcheck_tests/test_healthcheck.py index 51b6ce9672..87c65fc58f 100644 --- a/tests/playwright/test_healthcheck.py +++ b/tests/playwright/healthcheck_tests/test_healthcheck.py @@ -2,6 +2,6 @@ def test_dev_healthcheck(page: Page): - page.goto("https://dev-benefits.calitp.org/healthcheck") + page.goto("/healthcheck") expect(page.get_by_text("Healthy")).to_be_visible() diff --git a/tests/playwright/qa/__init__.py b/tests/playwright/qa/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/playwright/qa/config.yml b/tests/playwright/qa/config.yml new file mode 100644 index 0000000000..7bb4005721 --- /dev/null +++ b/tests/playwright/qa/config.yml @@ -0,0 +1,69 @@ +test: + - agency: "California State Transit" + flow: "Older Adult" + username: "username" + password: "password" + totp_secret: "secret" + card_user: "user" + card_number: "number" + card_expiration: "exp" + card_ccv: "ccv" + - agency: "California State Transit" + flow: "U.S. Veteran" + username: "username" + password: "password" + totp_secret: "secret" + card_user: "user" + card_number: "number" + card_expiration: "exp" + card_ccv: "ccv" + - agency: "California State Transit" + flow: "CalFresh Cardholder" + username: "username" + password: "password" + totp_secret: "secret" + card_user: "user" + card_number: "number" + card_expiration: "exp" + card_ccv: "ccv" + - agency: "California State Transit" + flow: "Agency Cardholder" + sub: "sub" + name: "name" + card_user: "user" + card_number: "number" + card_expiration: "exp" + card_ccv: "ccv" + - agency: "California State Transit" + flow: "Medicare Cardholder" + username: "username" + password: "password" + card_user: "user" + card_number: "number" + card_expiration: "exp" + card_ccv: "ccv" + +older_adult: + name: "Older Adult" + tests: + - agency: "California State Transit" + username: "username" + password: "password" + totp_secret: "secret" + card_user: "user" + card_number: "number" + card_expiration: "exp" + card_ccv: "ccv" + +veteran: + name: "U.S. Veteran" + tests: + - agency: "California State Transit" + name: "U.S. Veteran" + username: "username" + password: "password" + totp_secret: "secret" + card_user: "user" + card_number: "number" + card_expiration: "exp" + card_ccv: "ccv" diff --git a/tests/playwright/qa/conftest.py b/tests/playwright/qa/conftest.py new file mode 100644 index 0000000000..c529f689b0 --- /dev/null +++ b/tests/playwright/qa/conftest.py @@ -0,0 +1,55 @@ +from dataclasses import dataclass +import pytest + + +@dataclass +class AgencyFlow: + agency: str + flow: str + + +@dataclass +class AgencyCardCredentials: + sub: str + name: str + + +@dataclass +class PaymentCard: + cardholder_name: str + card_number: str + expiration: str + security_code: str + + +@pytest.fixture +def browser_context_args(): + return dict(user_agent="cal-itp/benefits-smoke-test") + + +@pytest.fixture +def agency_card_flows(): + return [ + ( + AgencyFlow(agency="California State Transit", flow="Agency Cardholder"), + AgencyCardCredentials(sub="71162", name="Box"), + ), + ( + AgencyFlow(agency="Monterey-Salinas Transit", flow="Courtesy Card"), + AgencyCardCredentials(sub="71162", name="Box"), + ), + ( + AgencyFlow(agency="Santa Barbara MTD", flow="Reduced Fare Mobility ID"), + AgencyCardCredentials(sub="1234", name="Barbara"), + ), + ] + + +@pytest.fixture +def valid_payment_card(): + return PaymentCard( + cardholder_name="Test User", + card_number="4111 1111 1111 1111", + expiration="12/34", + security_code="123", + ) diff --git a/tests/playwright/qa/models.py b/tests/playwright/qa/models.py new file mode 100644 index 0000000000..a51785b11b --- /dev/null +++ b/tests/playwright/qa/models.py @@ -0,0 +1,156 @@ +""" +Page object models for Playwright tests. + +https://playwright.dev/python/docs/pom +""" + +from playwright.sync_api import Page + +from .conftest import PaymentCard + + +class Index: + def __init__(self, page: Page): + self.page = page + + def select_agency(self, agency_name): + page = self.page + page.get_by_role("link", name="Choose your Provider").click() + page.get_by_role("link", name=agency_name).click() + + return EligibilityIndex(page) + + +class EligibilityIndex: + def __init__(self, page: Page): + self.page = page + + def select_flow(self, flow_name): + page = self.page + page.get_by_label(flow_name).check() + page.wait_for_load_state("networkidle") # wait for reCAPTCHA to finish loading + page.get_by_role("button", name="Choose this benefit").click() + + return EligibilityStart(page) + + +class EligibilityStart: + def __init__(self, page: Page): + self.page = page + + def click_continue(self): + page = self.page + page.get_by_role("link", name="Continue").click() + + return EligibilityConfirm(page) + + def get_started_with_login_gov(self): + page = self.page + page.get_by_role("link", name="Get started with Login.gov").click() + + return LoginGov(page) + + def continue_to_medicare_gov(self): + page = self.page + page.get_by_role("button", name="Continue to Medicare.gov").click() + + return MedicareGov(page) + + +class EligibilityConfirm: + def __init__(self, page: Page): + self.page = page + + def submit_form(self, sub, name): + page = self.page + page.locator("#sub").click() + + page.locator("#sub").fill(sub) + page.keyboard.press("Tab") + + page.locator("#name").fill(name) + + page.get_by_role("button", name="Find my record").click() + + return EnrollmentIndex(page) + + +class LoginGov: + def __init__(self, page: Page): + self.page = page + + def sign_in(self, username, password): + page = self.page + page.get_by_label("Email address").click() + page.get_by_label("Email address").fill(username) + page.get_by_label("Email address").press("Tab") + + page.get_by_label("Password", exact=True).fill(password) + + page.get_by_role("button", name="Sign in").click() + + def enter_otp(self, one_time_password): + page = self.page + page.get_by_label("One-time code").click() + page.get_by_label("One-time code").fill(one_time_password) + + page.get_by_role("button", name="Submit").click() + + return EnrollmentIndex(page) + + +class MedicareGov: + def __init__(self, page: Page): + self.page = page + + def log_in(self, username, password): + page = self.page + + page.get_by_label("Username", exact=True).click() + page.get_by_label("Username", exact=True).fill(username) + + page.get_by_label("Password").fill(password) + + page.get_by_role("button", name="Log in").click() + + def accept_consent_screen(self): + page = self.page + page.get_by_role("button", name="Connect").click() + + return EnrollmentIndex(page) + + +class EnrollmentIndex: + def __init__(self, page: Page): + self.page = page + + def enroll(self, payment_card: PaymentCard): + page = self.page + + with page.expect_popup() as popup_info: + page.get_by_role("button", name="Enroll").click() + + popup = popup_info.value + popup.wait_for_timeout(3000) + + popup.get_by_text("Cardholder name").click() + + popup.get_by_label("Cardholder name").fill(payment_card.cardholder_name) + popup.keyboard.press("Tab") + + popup.get_by_label("Card number").fill(payment_card.card_number) + popup.keyboard.press("Tab") + + popup.get_by_label("mm/yy").fill(payment_card.expiration) + popup.keyboard.press("Tab") + + popup.get_by_text("Security code", exact=True).click() + popup.get_by_label("Security code").fill(payment_card.security_code) + + # trigger form validation - not sure why their form behaves this way + popup.keyboard.press("Shift+Tab") + popup.keyboard.press("Shift+Tab") + popup.keyboard.press("Shift+Tab") + popup.keyboard.press("Tab") + + popup.get_by_role("group", name="Enter your card details").get_by_role("button").click() diff --git a/tests/playwright/qa/test_agency_flow.py b/tests/playwright/qa/test_agency_flow.py new file mode 100644 index 0000000000..0af99a7e96 --- /dev/null +++ b/tests/playwright/qa/test_agency_flow.py @@ -0,0 +1,25 @@ +from playwright.sync_api import BrowserContext, expect + +from .models import Index + + +def test_agency_card_flow(context: BrowserContext, base_url, agency_card_flows, valid_payment_card): + for agency_flow, cred in agency_card_flows: + print(agency_flow) + + page = context.new_page() + + page.goto(base_url) + + index = Index(page) + eligibility_index = index.select_agency(agency_flow.agency) + eligibility_start = eligibility_index.select_flow(agency_flow.flow) + eligibility_confirm = eligibility_start.click_continue() + enrollment_index = eligibility_confirm.submit_form(cred.sub, cred.name) + enrollment_index.enroll(valid_payment_card) + + # enrollment can take a bit + page.wait_for_timeout(15000) + + success_message = page.get_by_text("You can now use your contactless card to tap to ride with a reduced fare!") + expect(success_message).to_be_visible() diff --git a/tests/playwright/qa/test_login_gov_flow.py b/tests/playwright/qa/test_login_gov_flow.py new file mode 100644 index 0000000000..5854af1d3c --- /dev/null +++ b/tests/playwright/qa/test_login_gov_flow.py @@ -0,0 +1,93 @@ +import os + +from playwright.sync_api import BrowserContext, expect +import pyotp + +from .models import Index + + +def test_older_adult_flow(context: BrowserContext, base_url): + page = context.new_page() + + page.goto(base_url) + + index = Index(page) + eligibility_index = index.select_agency("California State Transit") + eligibility_start = eligibility_index.select_flow("Older Adult") + + login_gov = eligibility_start.get_started_with_login_gov() + + username = os.environ.get("PLAYWRIGHT_LOGIN_GOV_OLDER_ADULT_USERNAME") + password = os.environ.get("PLAYWRIGHT_LOGIN_GOV_OLDER_ADULT_PASSWORD") + login_gov.sign_in(username, password) + + authenticator_secret = os.environ.get("PLAYWRIGHT_LOGIN_GOV_OLDER_ADULT_AUTHENTICATOR_SECRET") + # create instance of "authenticator app" + totp = pyotp.TOTP(authenticator_secret) + enrollment_index = login_gov.enter_otp(totp.now()) + + enrollment_index.enroll("Test User", "4111 1111 1111 1111", "12/34", "123") + + # enrollment can take a bit + page.wait_for_timeout(10000) + + success_message = page.get_by_text("You can now use your contactless card to tap to ride with a reduced fare!") + expect(success_message).to_be_visible() + + +def test_us_veteran_flow(context: BrowserContext, base_url): + page = context.new_page() + + page.goto(base_url) + + index = Index(page) + eligibility_index = index.select_agency("California State Transit") + eligibility_start = eligibility_index.select_flow("U.S. Veteran") + + login_gov = eligibility_start.get_started_with_login_gov() + + username = os.environ.get("PLAYWRIGHT_LOGIN_GOV_VETERAN_USERNAME") + password = os.environ.get("PLAYWRIGHT_LOGIN_GOV_VETERAN_PASSWORD") + login_gov.sign_in(username, password) + + authenticator_secret = os.environ.get("PLAYWRIGHT_LOGIN_GOV_VETERAN_AUTHENTICATOR_SECRET") + # create instance of "authenticator app" + totp = pyotp.TOTP(authenticator_secret) + enrollment_index = login_gov.enter_otp(totp.now()) + + enrollment_index.enroll("Test User", "4111 1111 1111 1111", "12/34", "123") + + # enrollment can take a bit + page.wait_for_timeout(10000) + + success_message = page.get_by_text("You can now use your contactless card to tap to ride with a reduced fare!") + expect(success_message).to_be_visible() + + +def test_calfresh_cardholder_flow(context: BrowserContext, base_url): + page = context.new_page() + + page.goto(base_url) + + index = Index(page) + eligibility_index = index.select_agency("California State Transit") + eligibility_start = eligibility_index.select_flow("CalFresh Cardholder") + + login_gov = eligibility_start.get_started_with_login_gov() + + username = os.environ.get("PLAYWRIGHT_LOGIN_GOV_CALFRESH_USERNAME") + password = os.environ.get("PLAYWRIGHT_LOGIN_GOV_CALFRESH_PASSWORD") + login_gov.sign_in(username, password) + + authenticator_secret = os.environ.get("PLAYWRIGHT_LOGIN_GOV_CALFRESH_AUTHENTICATOR_SECRET") + # create instance of "authenticator app" + totp = pyotp.TOTP(authenticator_secret) + enrollment_index = login_gov.enter_otp(totp.now()) + + enrollment_index.enroll("Test User", "4111 1111 1111 1111", "12/34", "123") + + # enrollment can take a bit + page.wait_for_timeout(10000) + + reenrollment_error_message = page.get_by_text("You are still enrolled in this benefit") + expect(reenrollment_error_message).to_be_visible() diff --git a/tests/playwright/qa/test_medicare_flow.py b/tests/playwright/qa/test_medicare_flow.py new file mode 100644 index 0000000000..339d819c36 --- /dev/null +++ b/tests/playwright/qa/test_medicare_flow.py @@ -0,0 +1,32 @@ +import os +from playwright.sync_api import BrowserContext, expect + +from .models import Index + + +def test_medicare_cardholder_flow(context: BrowserContext, base_url): + page = context.new_page() + + page.goto(base_url) + + index = Index(page) + eligibility_index = index.select_agency("California State Transit") + eligibility_start = eligibility_index.select_flow("Medicare Cardholder") + + # avoid looking like a bot + page.add_init_script("delete Object.getPrototypeOf(navigator).webdriver") + + medicare_gov = eligibility_start.continue_to_medicare_gov() + + username = os.environ.get("PLAYWRIGHT_MEDICARE_GOV_USERNAME") + password = os.environ.get("PLAYWRIGHT_MEDICARE_GOV_PASSWORD") + medicare_gov.log_in(username, password) + enrollment_index = medicare_gov.accept_consent_screen() + + enrollment_index.enroll("Test User", "4111 1111 1111 1111", "12/34", "123") + + # enrollment can take a bit + page.wait_for_timeout(10000) + + success_message = page.get_by_text("You can now use your contactless card to tap to ride with a reduced fare!") + expect(success_message).to_be_visible() diff --git a/tests/playwright/requirements.txt b/tests/playwright/requirements.txt index 93497efddf..fc5f0f216b 100644 --- a/tests/playwright/requirements.txt +++ b/tests/playwright/requirements.txt @@ -1,4 +1,5 @@ playwright==1.51.0 # needs to match version on Docker image +pyotp pytest pytest-playwright pytest-reporter-html1 diff --git a/tests/playwright/run.sh b/tests/playwright/run.sh index d390fc1e27..4bb9559e7c 100755 --- a/tests/playwright/run.sh +++ b/tests/playwright/run.sh @@ -2,4 +2,6 @@ set -e -pytest +BASE_URL="https://dev-benefits.calitp.org" + +pytest --base-url $BASE_URL