diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 99ef30c3b..53af0de2d 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -7,12 +7,12 @@ permissions: env: NOTIFY_ENVIRONMENT: test - NEW_RELIC_CONFIG_FILE: newrelic.ini - NEW_RELIC_ENVIRONMENT: test FLASK_APP: application.py WERKZEUG_DEBUG_PIN: off REDIS_ENABLED: 0 NODE_VERSION: 16.15.1 + AWS_US_TOLL_FREE_NUMBER: "+18556438890" + ADMIN_BASE_URL: http://localhost:6012 jobs: build: @@ -44,29 +44,97 @@ jobs: run: npm test - name: Run py tests with coverage run: poetry run coverage run --omit=*/notifications_utils/* -m pytest --maxfail=10 --ignore=tests/end_to_end tests/ - # - name: Run E2E tests - # run: poetry run pytest -v --browser chromium --browser firefox --browser webkit tests/end_to_end - # env: - # NOTIFY_E2E_AUTH_STATE_PATH: ${{ secrets.NOTIFY_E2E_AUTH_STATE_PATH }} - # NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }} - # NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }} - # NOTIFY_E2E_TEST_URI: ${{ secrets.NOTIFY_E2E_TEST_URI }} - name: Check coverage threshold run: poetry run coverage report --fail-under=90 - # - name: Health check - # run: | - # response=$(curl -url ${{secrets.NOTIFY_E2E_TEST_URI}}_status) - # if grep -q "ok" <<< "$response"; then - # echo "Health check passed" - # else - # echo "Health check failed" - # exit 1 - # fi - # env: - # NOTIFY_E2E_AUTH_STATE_PATH: ${{ secrets.NOTIFY_E2E_AUTH_STATE_PATH }} - # NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }} - # NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }} - # NOTIFY_E2E_TEST_URI: ${{ secrets.NOTIFY_E2E_TEST_URI }} + + end-to-end-tests: + permissions: + checks: write + pull-requests: write + contents: write + runs-on: ubuntu-latest + services: + postgres: + image: postgres + env: + POSTGRES_USER: user + POSTGRES_PASSWORD: password + POSTGRES_DB: test_notification_api + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + # Maps tcp port 5432 on service container to the host + - 5432:5432 + redis: + image: redis + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + # Maps tcp port 6379 on service container to the host + - 6379:6379 + steps: + - uses: actions/checkout@v3 + - uses: ./.github/actions/setup-project + - uses: jwalton/gh-find-current-pr@v1 + id: findPr + - name: Clone API + uses: actions/checkout@v3 + with: + repository: GSA/notifications-api + path: 'notifications-api' + - name: Install API dependencies + working-directory: 'notifications-api' + run: make bootstrap + env: + DATABASE_URL: postgresql://user:password@localhost:5432/test_notification_api + SQLALCHEMY_DATABASE_TEST_URI: postgresql://user:password@localhost:5432/test_notification_api + REDIS_URL: redis://localhost:6379 + NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }} + NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }} + NOTIFY_ENVIRONMENT: development + - name: Run API server + working-directory: 'notifications-api' + run: make run-procfile & + env: + DATABASE_URL: postgresql://user:password@localhost:5432/test_notification_api + SQLALCHEMY_DATABASE_TEST_URI: postgresql://user:password@localhost:5432/test_notification_api + REDIS_URL: redis://localhost:6379 + NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }} + NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }} + NOTIFY_ENVIRONMENT: development + - name: Run Admin server + run: make run-flask & + env: + # API_HOST_NAME: https://notify-api-staging.app.cloud.gov + API_HOST_NAME: http://localhost:6011 + DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }} + SECRET_KEY: ${{ secrets.SECRET_KEY }} + ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }} + ADMIN_CLIENT_USERNAME: notify-admin + NOTIFY_ENVIRONMENT: e2etest + NOTIFY_E2E_AUTH_STATE_PATH: ${{ secrets.NOTIFY_E2E_AUTH_STATE_PATH }} + NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }} + NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }} + NOTIFY_E2E_TEST_URI: http://localhost:6012 + - name: Run E2E tests + # Run the E2E tests against the code found in this PR. + # run: poetry run pytest -v --browser chromium --browser firefox --browser webkit tests/end_to_end + # --browser webkit doesn't work at this time. + run: make e2e-test + # Debugging for now to troubleshoot a connectivity issue to the local servers + # run: curl --request GET --url "http://localhost:6012" + env: + NOTIFY_ENVIRONMENT: e2etest + NOTIFY_E2E_AUTH_STATE_PATH: ${{ secrets.NOTIFY_E2E_AUTH_STATE_PATH }} + NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }} + NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }} + NOTIFY_E2E_TEST_URI: http://localhost:6012 validate-new-relic-config: runs-on: ubuntu-latest @@ -76,6 +144,7 @@ jobs: - uses: ./.github/actions/setup-project - name: Validate NewRelic config env: + NEW_RELIC_CONFIG_FILE: newrelic.ini NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }} # Need to set a NEW_RELIC_ENVIRONMENT with monitor_mode: true NEW_RELIC_ENVIRONMENT: staging diff --git a/Makefile b/Makefile index 8f950dbf6..aa9b404af 100644 --- a/Makefile +++ b/Makefile @@ -80,8 +80,8 @@ dead-code: .PHONY: e2e-test e2e-test: export NEW_RELIC_ENVIRONMENT=test -e2e-test: ## Run end-to-end integration tests - poetry run pytest -v --browser chromium --browser firefox --browser webkit tests/end_to_end +e2e-test: ## Run end-to-end integration tests; note that --browser webkit isn't currently working + poetry run pytest -v --browser chromium --browser firefox tests/end_to_end .PHONY: js-lint js-lint: ## Run javascript linting scanners diff --git a/app/config.py b/app/config.py index 91c514509..02394aa84 100644 --- a/app/config.py +++ b/app/config.py @@ -82,8 +82,7 @@ class Config(object): "IBAN": "GB33BUKB20201555555555", "swift": "ABCDEF12", "notify_billing_email_addresses": [ - "generic@digital.cabinet-office.gov.uk", - "first.last@digital.cabinet-office.gov.uk", + "tts-benefits-studio@gsa.gov", ], } @@ -153,6 +152,34 @@ class Staging(Production): HEADER_COLOUR = "#00ff00" # $green +class E2ETest(Staging): + """ + An environment config that is intended to operate as if it were in the + staging environment but with the configuration of the development and test + environments so the E2E tests work. + """ + + # Borrowed from development environment + DEBUG = True + SESSION_COOKIE_SECURE = False + SESSION_PROTECTION = None + HTTP_PROTOCOL = "http" + ASSET_DOMAIN = "" + ASSET_PATH = "/static/" + + # Borrowed from test environment + TESTING = True + WTF_CSRF_ENABLED = False + + # buckets - mirror staging + CSV_UPLOAD_BUCKET = cloud_config.s3_credentials( + "notify-api-csv-upload-bucket-staging" + ) + LOGO_UPLOAD_BUCKET = cloud_config.s3_credentials( + "notify-admin-logo-upload-bucket-staging" + ) + + class Demo(Staging): HEADER_COLOUR = "#6F72AF" # $mauve @@ -174,6 +201,7 @@ class Scanning(Production): configs = { "development": Development, "test": Test, + "e2etest": E2ETest, "scanning": Scanning, "staging": Staging, "demo": Demo, diff --git a/tests/conftest.py b/tests/conftest.py index 9019c2293..4c2ceeea9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,6 @@ import copy import json import os -import re from contextlib import contextmanager from datetime import date, datetime, timedelta from unittest.mock import Mock, PropertyMock @@ -529,7 +528,7 @@ def mock_update_service(mocker): "sms_sender", "permissions", ] - } + }, ) return {"data": service} @@ -2355,7 +2354,7 @@ def client_request(logged_in_client, mocker, service_one): # noqa (C901 too com _test_page_title=True, _test_for_elements_without_class=True, _optional_args="", - **endpoint_kwargs + **endpoint_kwargs, ): return ClientRequest.get_url( url_for(endpoint, **(endpoint_kwargs or {})) + _optional_args, @@ -2374,7 +2373,7 @@ def client_request(logged_in_client, mocker, service_one): # noqa (C901 too com _expected_redirect=None, _test_page_title=True, _test_for_elements_without_class=True, - **endpoint_kwargs + **endpoint_kwargs, ): resp = logged_in_client.get( url, @@ -2416,7 +2415,7 @@ def client_request(logged_in_client, mocker, service_one): # noqa (C901 too com _follow_redirects=False, _expected_redirect=None, _content_type=None, - **endpoint_kwargs + **endpoint_kwargs, ): return ClientRequest.post_url( url_for(endpoint, **(endpoint_kwargs or {})), @@ -2475,7 +2474,7 @@ def client_request(logged_in_client, mocker, service_one): # noqa (C901 too com _expected_status=302, _optional_args="", _content_type=None, - **endpoint_kwargs + **endpoint_kwargs, ): return ClientRequest.post_response_from_url( url_for(endpoint, **(endpoint_kwargs or {})) + _optional_args, @@ -3521,89 +3520,6 @@ def mock_get_invited_org_user_by_id(mocker, sample_org_invite): ) -def login_for_end_to_end_testing(browser): - # Open a new page and go to the staging site. - context = browser.new_context() - page = context.new_page() - page.goto(os.getenv("NOTIFY_E2E_TEST_URI")) - - sign_in_button = page.get_by_role("link", name="Sign in") - - # Test trying to sign in. - sign_in_button.click() - - # Wait for the next page to fully load. - page.wait_for_load_state("domcontentloaded") - - # Check for the sign in form elements. - # NOTE: Playwright cannot find input elements by role and recommends using - # get_by_label() instead; however, hidden form elements do not have - # labels associated with them, hence the XPath! - # See https://playwright.dev/python/docs/api/class-page#page-get-by-label - # and https://playwright.dev/python/docs/locators#locate-by-css-or-xpath - # for more information. - email_address_input = page.get_by_label("Email address") - password_input = page.get_by_label("Password") - continue_button = page.get_by_role("button", name=re.compile("Continue")) - - # Sign in to the site. - email_address_input.fill(os.getenv("NOTIFY_E2E_TEST_EMAIL")) - password_input.fill(os.getenv("NOTIFY_E2E_TEST_PASSWORD")) - continue_button.click() - - # Wait for the next page to fully load. - page.wait_for_load_state("domcontentloaded") - - # Check for the sign in form elements. - # NOTE: Playwright cannot find input elements by role and recommends using - # get_by_label() instead; however, hidden form elements do not have - # labels associated with them, hence the XPath! - # See https://playwright.dev/python/docs/api/class-page#page-get-by-label - # and https://playwright.dev/python/docs/locators#locate-by-css-or-xpath - # for more information. - # mfa_input = page.get_by_label('Text message code') - # continue_button = page.get_by_role('button', name=re.compile('Continue')) - - # # Enter MFA code and continue. - # TODO: Revisit this at a later point in time. - # totp = pyotp.TOTP( - # os.getenv('MFA_TOTP_SECRET'), - # digits=int(os.getenv('MFA_TOTP_LENGTH')) - # ) - - # mfa_input.fill(totp.now()) - # continue_button.click() - - # page.wait_for_load_state('domcontentloaded') - - # # Save storage state into the file. - # auth_state_path = os.path.join( - # os.getenv('NOTIFY_E2E_AUTH_STATE_PATH'), - # 'state.json' - # ) - # context.storage_state(path=auth_state_path) - - -@pytest.fixture(scope="session") -def end_to_end_context(browser): - context = browser.new_context() - return context - - -@pytest.fixture(scope="session") -def end_to_end_authenticated_context(browser): - # Create and load a previously authenticated context for Playwright E2E - # tests. - login_for_end_to_end_testing(browser) - - auth_state_path = os.path.join( - os.getenv("NOTIFY_E2E_AUTH_STATE_PATH"), "state.json" - ) - context = browser.new_context(storage_state=auth_state_path) - - return context - - @pytest.fixture() def fake_markdown_file(): input = "#Test" diff --git a/tests/end_to_end/__init__.py b/tests/end_to_end/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/end_to_end/conftest.py b/tests/end_to_end/conftest.py new file mode 100644 index 000000000..48d1bcd35 --- /dev/null +++ b/tests/end_to_end/conftest.py @@ -0,0 +1,105 @@ +import os +import re + +import pytest + +E2E_TEST_URI = os.getenv("NOTIFY_E2E_TEST_URI") + + +def login_for_end_to_end_testing(browser): + # Open a new page and go to the staging site. + context = browser.new_context() + page = context.new_page() + page.goto(f"{E2E_TEST_URI}/") + + sign_in_button = page.get_by_role("link", name="Sign in") + + # Test trying to sign in. + sign_in_button.click() + + # Wait for the next page to fully load. + page.wait_for_load_state("domcontentloaded") + + # Check for the sign in form elements. + # NOTE: Playwright cannot find input elements by role and recommends using + # get_by_label() instead; however, hidden form elements do not have + # labels associated with them, hence the XPath! + # See https://playwright.dev/python/docs/api/class-page#page-get-by-label + # and https://playwright.dev/python/docs/locators#locate-by-css-or-xpath + # for more information. + email_address_input = page.get_by_label("Email address") + password_input = page.get_by_label("Password") + continue_button = page.get_by_role("button", name=re.compile("Continue")) + + # Sign in to the site. + email_address_input.fill(os.getenv("NOTIFY_E2E_TEST_EMAIL")) + password_input.fill(os.getenv("NOTIFY_E2E_TEST_PASSWORD")) + continue_button.click() + + # Wait for the next page to fully load. + page.wait_for_load_state("domcontentloaded") + + # Check for the sign in form elements. + # NOTE: Playwright cannot find input elements by role and recommends using + # get_by_label() instead; however, hidden form elements do not have + # labels associated with them, hence the XPath! + # See https://playwright.dev/python/docs/api/class-page#page-get-by-label + # and https://playwright.dev/python/docs/locators#locate-by-css-or-xpath + # for more information. + # mfa_input = page.get_by_label('Text message code') + # continue_button = page.get_by_role('button', name=re.compile('Continue')) + + # # Enter MFA code and continue. + # TODO: Revisit this at a later point in time. + # totp = pyotp.TOTP( + # os.getenv('MFA_TOTP_SECRET'), + # digits=int(os.getenv('MFA_TOTP_LENGTH')) + # ) + + # mfa_input.fill(totp.now()) + # continue_button.click() + + # page.wait_for_load_state('domcontentloaded') + + # Save storage state into the file. + auth_state_path = os.path.join( + os.getenv("NOTIFY_E2E_AUTH_STATE_PATH"), "state.json" + ) + context.storage_state(path=auth_state_path) + + +@pytest.fixture(scope="session") +def end_to_end_context(browser): + context = browser.new_context() + return context + + +@pytest.fixture(scope="session") +def end_to_end_authenticated_context(browser): + # Create and load a previously authenticated context for Playwright E2E + # tests. + # login_for_end_to_end_testing(browser) + + auth_state_path = os.path.join( + os.getenv("NOTIFY_E2E_AUTH_STATE_PATH"), "state.json" + ) + context = browser.new_context(storage_state=auth_state_path) + + return context + + +@pytest.fixture(scope="session") +def authenticated_page(end_to_end_context): + # Open a new page and go to the staging site. + page = end_to_end_context.new_page() + + page.goto(f"{E2E_TEST_URI}/") + + sign_in_button = page.get_by_role("link", name="Sign in") + + # Sign in to the site - E2E test accounts are set to flow through. + sign_in_button.click() + + # Wait for the next page to fully load. + page.wait_for_load_state("domcontentloaded") + return page diff --git a/tests/end_to_end/test_accounts_page.py b/tests/end_to_end/test_accounts_page.py index c8c8d5a6a..411728e33 100644 --- a/tests/end_to_end/test_accounts_page.py +++ b/tests/end_to_end/test_accounts_page.py @@ -4,33 +4,13 @@ import re from playwright.sync_api import expect - -def _bypass_sign_in(end_to_end_context): - # Open a new page and go to the staging site. - page = end_to_end_context.new_page() - - page.goto(os.getenv("NOTIFY_E2E_TEST_URI")) - - sign_in_button = page.get_by_role("link", name="Sign in") - - # Test trying to sign in. Because we are loading the email and password - sign_in_button.click() - - # Wait for the next page to fully load. - page.wait_for_load_state("domcontentloaded") - return page +E2E_TEST_URI = os.getenv("NOTIFY_E2E_TEST_URI") -def test_add_new_service_workflow(end_to_end_context): - # page = end_to_end_context.new_page() - page = _bypass_sign_in(end_to_end_context) - page.goto(os.getenv("NOTIFY_E2E_TEST_URI")) +def test_add_new_service_workflow(authenticated_page, end_to_end_context): + page = authenticated_page + page.goto(f"{E2E_TEST_URI}/") - # sign_in_button = page.get_by_role("link", name="Sign in") - # - # Test trying to sign in. Because we are loading the email and password - # sign_in_button.click() - # # Wait for the next page to fully load. page.wait_for_load_state("domcontentloaded") @@ -41,9 +21,7 @@ def test_add_new_service_workflow(end_to_end_context): browser_type=end_to_end_context.browser.browser_type.name, ) - accounts_uri = "{}accounts".format(os.getenv("NOTIFY_E2E_TEST_URI")) - - page.goto(accounts_uri) + page.goto(f"{E2E_TEST_URI}/accounts") # Check to make sure that we've arrived at the next page. page.wait_for_load_state("domcontentloaded") @@ -82,24 +60,13 @@ def test_add_new_service_workflow(end_to_end_context): # Retrieve some prominent elements on the page for testing. service_name_input = page.locator('xpath=//input[@name="name"]') - federal_radio_button = page.locator('xpath=//input[@value="federal"]') - state_radio_button = page.locator('xpath=//input[@value="state"]') - other_radio_button = page.locator('xpath=//input[@value="other"]') add_service_button = page.get_by_role("button", name=re.compile("Add service")) expect(service_name_input).to_be_visible() - expect(federal_radio_button).to_be_visible() - expect(state_radio_button).to_be_visible() - expect(other_radio_button).to_be_visible() expect(add_service_button).to_be_visible() # Fill in the form. service_name_input.fill(new_service_name) - expect(federal_radio_button).to_be_enabled() - # Trying to click directly on the radio button resulted in a "not in viewport error" and this is the - # suggested workaround. Googling, the reason seems to be that there might be some (invisible?) css positioned - # above the radio button itself. - page.click("text='Federal government'") # Click on add service. add_service_button.click() @@ -108,7 +75,7 @@ def test_add_new_service_workflow(end_to_end_context): page.wait_for_load_state("domcontentloaded") # Check for the service name title and heading. - service_heading = page.get_by_text(new_service_name) + service_heading = page.get_by_text(new_service_name, exact=True) expect(service_heading).to_be_visible() expect(page).to_have_title(re.compile(new_service_name)) diff --git a/tests/end_to_end/test_landing_and_sign_in_pages.py b/tests/end_to_end/test_landing_and_sign_in_pages.py index 895b3851f..dd6a70beb 100644 --- a/tests/end_to_end/test_landing_and_sign_in_pages.py +++ b/tests/end_to_end/test_landing_and_sign_in_pages.py @@ -3,11 +3,13 @@ import re from playwright.sync_api import expect +E2E_TEST_URI = os.getenv("NOTIFY_E2E_TEST_URI") + def test_landing_page(end_to_end_context): # Open a new page and go to the staging site. page = end_to_end_context.browser.new_page() - page.goto(os.getenv("NOTIFY_E2E_TEST_URI")) + page.goto(f"{E2E_TEST_URI}/") # Check to make sure that we've arrived at the next page. page.wait_for_load_state("domcontentloaded") @@ -17,7 +19,8 @@ def test_landing_page(end_to_end_context): # Retrieve some prominent elements on the page for testing. main_header = page.get_by_role( - "heading", name="Send text messages to your participants" + "heading", + name="Reach people where they are with government-powered text messages", ) sign_in_button = page.get_by_role("link", name="Sign in") benefits_studio_email = page.get_by_role("link", name="tts-benefits-studio@gsa.gov") @@ -51,7 +54,7 @@ def test_landing_page(end_to_end_context): # def test_sign_in_and_mfa_pages(end_to_end_context): # # Open a new page and go to the staging site. # page = end_to_end_context.new_page() -# page.goto(os.getenv("NOTIFY_E2E_TEST_URI")) +# page.goto(f"{E2E_TEST_URI}/") # print(f"test_sign_in_and_mfa_pages initial {page}") # # sign_in_button = page.get_by_role("link", name="Sign in")