diff --git a/README.md b/README.md index 606c4ae2a..b013bbeac 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,9 @@ The e2e test for find currently requires a `test`-scoped API key for GOV.UK Noti the test. Pass an environment variable called `E2E_NOTIFY_FIND_API_KEY` to allow this test to pass. If you want to run the end-to-end tests against our deployed dev/test environments, you can do this by adding the -`--e2e-env` flag to the pytest command with a value of either `dev` or `test`. +`--e2e-env` flag to the pytest command with a value of either `dev` or `test`, and `--e2e-aws-vault-profile` with a value +that matches the aws-vault profile name for the matching environment. The tests expect a session to be available +without any input, so you must have authenticated already and have your credentials cached. ## Updating database migrations diff --git a/tests/conftest.py b/tests/conftest.py index 9ce69bb92..a67d6f1ae 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -54,6 +54,9 @@ def pytest_addoption(parser): parser.addoption("--e2e", action="store_true", default=False, help="run e2e (browser) tests") + + # WARNING: Do not add an option for `prod` here. We *must* rework the e2e test authentication process before + # that would be something we could consider. parser.addoption( "--e2e-env", action="store", @@ -61,6 +64,13 @@ def pytest_addoption(parser): help="choose the environment that e2e tests will target", choices=("local", "dev", "test"), ) + + parser.addoption( + "--e2e-aws-vault-profile", + action="store", + help="the aws-vault profile matching the env set in --e2e-env (for `dev` or `test` only)", + ) + parser.addoption( "--viewport", default="1920x1080", diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index 90e78c729..957492215 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -1,13 +1,19 @@ +import base64 +import datetime +import json +import subprocess +import uuid +from pathlib import Path + +import boto3 +import jwt import pytest -import requests -from playwright._impl._errors import Error as PlaywrightError -from playwright.sync_api import Page +from playwright.sync_api import Browser, BrowserContext, Page from pytest import FixtureRequest from config import Config from tests.e2e_tests.dataclasses import Account, FundingServiceDomains, TestFundConfig -from tests.e2e_tests.helpers import create_account_with_roles, generate_email_address -from tests.e2e_tests.pages.authenticator import MagicLinkPage, NewMagicLinkPage +from tests.e2e_tests.helpers import generate_email_address @pytest.fixture(autouse=True) @@ -22,18 +28,21 @@ def domains(request: FixtureRequest) -> FundingServiceDomains: if e2e_env == "local": return FundingServiceDomains( + cookie=".levellingup.gov.localhost", authenticator=f"http://{Config.AUTHENTICATOR_HOST}.levellingup.gov.localhost:4004", find=f"http://{Config.FIND_HOST}", submit=f"http://{Config.SUBMIT_HOST}", ) elif e2e_env == "dev": return FundingServiceDomains( + cookie=".dev.access-funding.test.levellingup.gov.uk", authenticator="https://authenticator.dev.access-funding.test.levellingup.gov.uk", find="https://find-monitoring-data.dev.access-funding.test.levellingup.gov.uk", submit="https://submit-monitoring-data.dev.access-funding.test.levellingup.gov.uk", ) elif e2e_env == "test": return FundingServiceDomains( + cookie=".test.access-funding.test.levellingup.gov.uk", authenticator="https://authenticator.test.access-funding.test.levellingup.gov.uk", find="https://find-monitoring-data.test.access-funding.test.levellingup.gov.uk", submit="https://submit-monitoring-data.test.access-funding.test.levellingup.gov.uk", @@ -61,6 +70,47 @@ def authenticator_fund_config(request: FixtureRequest) -> TestFundConfig: raise ValueError(f"not configured for {e2e_env}") +def _read_jwt_signing_key(e2e_env: str, e2e_aws_vault_profile: str | None): + """Reads the RS256 private key used to sign JWTs in our development environments. + + This is an (unpleasant) workaround for our authentication journey not providing a suitable alternative. + """ + if e2e_env == "local": + _test_private_key_path = str(Path(__file__).parent.parent) + "/keys/rsa256/private.pem" + with open(_test_private_key_path, mode="rb") as private_key_file: + rsa256_private_key = private_key_file.read() + + elif e2e_env in {"dev", "test"}: + param_name = f"/copilot/pre-award/{e2e_env}/secrets/RSA256_PRIVATE_KEY_BASE64" + if e2e_aws_vault_profile: # This flow is used locally + rsa256_private_key_base64 = json.loads( + subprocess.check_output( + [ + "aws-vault", + "exec", + e2e_aws_vault_profile, + "--", + "aws", + "ssm", + "get-parameter", + "--name", + param_name, + "--with-decryption", + ], + ).decode() + )["Parameter"]["Value"] + + else: # This flow is used in CI/CD where credentials are available implicitly + ssm_client = boto3.client("ssm") + rsa256_private_key_base64 = ssm_client.get_parameter(Name=param_name, WithDecryption=True)["Parameter"][ + "Value" + ] + rsa256_private_key = base64.b64decode(rsa256_private_key_base64) + del rsa256_private_key_base64 + + return rsa256_private_key + + @pytest.fixture def context(request: FixtureRequest, browser: Browser): e2e_env = request.config.getoption("e2e_env") @@ -83,45 +133,46 @@ def user_auth( request: FixtureRequest, domains: FundingServiceDomains, authenticator_fund_config: TestFundConfig, - page: Page, + context: BrowserContext, ) -> Account: + e2e_env = request.config.getoption("e2e_env") + e2e_aws_vault_profile = request.config.getoption("e2e_aws_vault_profile") + email_address = generate_email_address( test_name=request.node.originalname, email_domain="communities.gov.uk", ) - user_roles = request.node.get_closest_marker("user_roles") - - account = create_account_with_roles( - email_address=email_address, - roles=user_roles.args[0] if user_roles else [], + roles_marker = request.node.get_closest_marker("user_roles") + user_roles = roles_marker.args[0] if roles_marker else [] + + now = int(datetime.datetime.timestamp(datetime.datetime.now())) + jwt_data = { + "accountId": str(uuid.uuid4()), + "azureAdSubjectId": str(uuid.uuid4()), + "email": email_address, + "fullName": f"E2E Test User - {request.node.originalname}", + "roles": user_roles, + "iat": now, + "exp": now + (15 * 60), # 15 minutes from now + } + + def create_token(payload): + algorithm = "RS256" # Must match the algorithm used by fsd-authenticator + return jwt.encode(payload, _read_jwt_signing_key(e2e_env, e2e_aws_vault_profile), algorithm=algorithm) + + context.add_cookies( + [ + { + "name": Config.FSD_USER_TOKEN_COOKIE_NAME, + "value": create_token(jwt_data), + "domain": domains.cookie, + "path": "/", + "httpOnly": True, + "secure": True, + "sameSize": "Strict", + } + ] ) - response = requests.get(f"{domains.authenticator}/magic-links") - magic_links_before = set(response.json()) - - new_magic_link_page = NewMagicLinkPage(page, domain=domains.authenticator, fund_config=authenticator_fund_config) - new_magic_link_page.navigate() - new_magic_link_page.insert_email_address(account.email_address) - new_magic_link_page.press_continue() - - response = requests.get(f"{domains.authenticator}/magic-links") - magic_links_after = set(response.json()) - - new_magic_links = magic_links_after - magic_links_before - for magic_link in new_magic_links: - if magic_link.startswith("link:"): - break - else: - raise KeyError("Could not generate/retrieve a new magic link via authenticator") - - magic_link_id = magic_link.split(":")[1] - magic_link_page = MagicLinkPage(page, domain=domains.authenticator) - - try: - magic_link_page.navigate(magic_link_id) - except PlaywrightError: - # FIXME: Authenticator gets into a weird redirect loop locally... We just ignore that error. - pass - - return account + return Account(email_address=email_address, roles=user_roles) diff --git a/tests/e2e_tests/dataclasses.py b/tests/e2e_tests/dataclasses.py index 2cc37f104..56ac7536f 100644 --- a/tests/e2e_tests/dataclasses.py +++ b/tests/e2e_tests/dataclasses.py @@ -9,6 +9,7 @@ class Account: @dataclass class FundingServiceDomains: + cookie: str authenticator: str find: str submit: str