Skip to content

Commit b628ba6

Browse files
committed
VED-1013 Part 3/4 Add support for running new e2e tests in PR environments (#1140)
1 parent fdc6555 commit b628ba6

7 files changed

Lines changed: 207 additions & 1 deletion

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
auth_url=https://internal-dev.api.service.nhs.uk/oauth2-mock/authorize
2+
token_url=https://internal-dev.api.service.nhs.uk/oauth2-mock/token
3+
callback_url=https://oauth.pstmn.io/v1/callback
4+
# Obtain value from dev/testing team
5+
STATUS_API_KEY=
6+
7+
username=aal3
8+
scope=nhs-cis2
9+
10+
# Internal-dev PR environment
11+
baseUrl=https://internal-dev.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4-pr-123
12+
aws_token_refresh=False
13+
aws_profile_name=345594581768_DEV-IMMS-Devops
14+
15+
S3_env=pr-123
16+
# LOCAL_RUN_WITHOUT_S3_UPLOAD = True
17+
LOCAL_RUN_FILE_NAME=HPV_Vaccinations_v5_V0V8L_20251111T16304982.csv
18+
AWS_DOMAIN_NAME=https://pr-123.imms.dev.vds.platform.nhs.uk
19+
20+
PROXY_NAME=immunisation-fhir-api-pr-123
21+
# See README for details on how to obtain this
22+
APIGEE_ACCESS_TOKEN={use-the-apigee-get-token-utility}
23+
APIGEE_USERNAME={your-apigee-developer-email}

tests/e2e_automation/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,22 @@ This directory contains End-to-end Automation Tests for the Immunisation FHIR AP
5959
- `make test-batch-full` - run Batch tests
6060
- `make test-batch-smoke` - run Batch smoke tests only (quicker)
6161
- `make collect-only` - check that all tests are discovered
62+
63+
## Running e2e_automation tests against PR environments
64+
65+
The environment variables define a client ID and client secret for each of the Apigee test apps we use in static
66+
environments such as `internal-dev`, `internal-qa` and so on.
67+
68+
However, creating pull requests will spin up a dynamic Apigee proxy and AWS backend which lives for the duration of the PR.
69+
To minimise admin overhead, the automation tests create dynamic applications for the duration of a test run rather than
70+
us having to manually create new apps each time we produce a pull request.
71+
72+
These tests are run seamlessly in the pipeline. But if you are doing some local changes and want to test against your
73+
PR environment, please follow these pre-requisites to get it working:
74+
75+
1. [Install](https://docs.apigee.com/api-platform/system-administration/auth-tools#install) and run the Apigee [get_token](https://docs.apigee.com/api-platform/system-administration/using-gettoken) tool to obtain an access token.
76+
2. Set this value against the `APIGEE_ACCESS_TOKEN` in your .env file.
77+
3. Finally, use the [.env.example.pr](./.env.example.pr) as your baseline for your .env file and fill all of the required values.
78+
79+
Note: the `get_token` tool is only supported in Linux environments, so if you are using a Windows environment, you will
80+
at least need to run the operation in WSL to obtain the access token.

tests/e2e_automation/features/conftest.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
from utilities.api_fhir_immunization_helper import empty_folder
88
from utilities.api_gen_token import get_tokens
99
from utilities.api_get_header import get_delete_url_header
10+
from utilities.apigee.apigee_env_helpers import is_pr_env
11+
from utilities.apigee.ApigeeApp import ApigeeApp
12+
from utilities.apigee.ApigeeOnDemandAppManager import ApigeeOnDemandAppManager
1013
from utilities.aws_token import refresh_sso_token, set_aws_session_token
1114
from utilities.context import ScenarioContext
1215
from utilities.enums import SupplierNameWithODSCode
@@ -53,8 +56,25 @@ def global_context():
5356
).strip().lower() == "true" else set_aws_session_token()
5457

5558

59+
@pytest.fixture(scope="session")
60+
def temp_apigee_apps():
61+
if is_pr_env():
62+
apigee_app_mgr = ApigeeOnDemandAppManager()
63+
created_apps = apigee_app_mgr.setup_apps_and_product()
64+
65+
for test_app in created_apps:
66+
os.environ[f"{test_app.supplier}_client_Id"] = test_app.client_id
67+
os.environ[f"{test_app.supplier}_client_Secret"] = test_app.client_secret
68+
69+
yield created_apps
70+
71+
apigee_app_mgr.teardown_apps_and_product()
72+
else:
73+
yield None
74+
75+
5676
@pytest.fixture
57-
def context(request, global_context) -> ScenarioContext:
77+
def context(request, global_context, temp_apigee_apps: list[ApigeeApp] | None) -> ScenarioContext:
5878
ctx = ScenarioContext()
5979
ctx.aws_profile_name = os.getenv("aws_profile_name")
6080

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""Simple data class to hold the required attributes of an Apigee App"""
2+
3+
from dataclasses import dataclass
4+
5+
6+
@dataclass
7+
class ApigeeApp:
8+
callback_url: str
9+
client_id: str
10+
client_secret: str
11+
supplier: str
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"""Basic client class for managing interactions with the Apigee API"""
2+
3+
import uuid
4+
5+
import requests
6+
7+
from utilities.apigee.apigee_env_helpers import get_apigee_access_token, get_apigee_username, get_proxy_name
8+
from utilities.apigee.ApigeeApp import ApigeeApp
9+
10+
11+
class ApigeeOnDemandAppManager:
12+
"""Manager class that provides required Apigee functionality for PR env e2e tests. E.g. creating an app, subscribing it to
13+
products and teardown"""
14+
15+
# We only use the Apigee API in the non-prod organisation and the internal-dev environment
16+
_BASE_URL = "https://api.enterprise.apigee.com/v1/organizations/nhsd-nonprod"
17+
_APPS_PATH = "apps"
18+
_DEVELOPERS_PATH = "developers"
19+
_PRODUCTS_PATH = "apiproducts"
20+
_INTERNAL_DEV_ENV_NAME = "internal-dev"
21+
_TEST_APP_SUPPLIERS = ("EMIS", "MAVIS", "MEDICUS", "Postman_Auth", "RAVS", "SONAR", "TPP")
22+
23+
def __init__(self):
24+
self.pr_proxy_name = get_proxy_name()
25+
self.created_product_name_uuid: str = ""
26+
self.created_app_name_uuids = []
27+
self.display_name = f"test-{self.pr_proxy_name}"
28+
29+
self.logged_in_username = get_apigee_username()
30+
self.access_token = get_apigee_access_token()
31+
32+
self.requests_session = requests.Session()
33+
self.requests_session.headers.update({"Authorization": f"Bearer {self.access_token}"})
34+
35+
def _create_app(self, target_product_name: str, supplier_name: str) -> ApigeeApp:
36+
app_name_uuid = str(uuid.uuid4())
37+
app_data = {
38+
"name": app_name_uuid,
39+
"callbackUrl": "https://oauth.pstmn.io/v1/callback",
40+
"status": "approved",
41+
"attributes": [
42+
{"name": "DisplayName", "value": f"{self.display_name}-{supplier_name}"},
43+
{"name": "SupplierSystem", "value": supplier_name},
44+
],
45+
"apiProducts": [target_product_name, "identity-service-internal-dev"],
46+
}
47+
48+
response = self.requests_session.post(
49+
url=f"{self._BASE_URL}/{self._DEVELOPERS_PATH}/{self.logged_in_username}/{self._APPS_PATH}", json=app_data
50+
)
51+
response.raise_for_status()
52+
53+
self.created_app_name_uuids.append(app_name_uuid)
54+
response_dict = response.json()
55+
56+
return ApigeeApp(
57+
callback_url=response_dict.get("callbackUrl"),
58+
client_id=response_dict["credentials"][0]["consumerKey"],
59+
client_secret=response_dict["credentials"][0]["consumerSecret"],
60+
supplier=supplier_name,
61+
)
62+
63+
def _create_product(self) -> str:
64+
product_name_uuid = str(uuid.uuid4())
65+
apigee_product_data = {
66+
"name": product_name_uuid,
67+
"apiResources": [],
68+
"approvalType": "auto",
69+
"description": "Autogenerated API product for E2E tests",
70+
"displayName": self.display_name,
71+
"environments": [self._INTERNAL_DEV_ENV_NAME],
72+
"proxies": [self.pr_proxy_name],
73+
"scopes": [
74+
f"urn:nhsd:apim:app:level3:{self.pr_proxy_name}",
75+
f"urn:nhsd:apim:user-nhs-cis2:aal3:{self.pr_proxy_name}",
76+
],
77+
}
78+
79+
response = self.requests_session.post(
80+
url=f"{self._BASE_URL}/{self._PRODUCTS_PATH}",
81+
json=apigee_product_data,
82+
)
83+
response.raise_for_status()
84+
85+
self.created_product_name_uuid = product_name_uuid
86+
return product_name_uuid
87+
88+
def setup_apps_and_product(self) -> list[ApigeeApp]:
89+
"""Orchestration method to setup the required product and on-demand apps required for PR testing"""
90+
created_apps: list[ApigeeApp] = []
91+
product_name_uuid = self._create_product()
92+
93+
for supplier_name in self._TEST_APP_SUPPLIERS:
94+
created_apps.append(self._create_app(product_name_uuid, supplier_name))
95+
96+
return created_apps
97+
98+
def teardown_apps_and_product(self):
99+
"""Orchestration method to remove the Apigee resources in a teardown step"""
100+
for created_app_name_uuid in self.created_app_name_uuids:
101+
self.requests_session.delete(
102+
url=f"{self._BASE_URL}/{self._DEVELOPERS_PATH}/{self.logged_in_username}/{self._APPS_PATH}/{created_app_name_uuid}"
103+
)
104+
105+
self.requests_session.delete(url=f"{self._BASE_URL}/{self._PRODUCTS_PATH}/{self.created_product_name_uuid}")

tests/e2e_automation/utilities/apigee/__init__.py

Whitespace-only changes.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import os
2+
3+
4+
def get_env_var(var_name: str) -> str:
5+
value = os.getenv(var_name)
6+
7+
if not value:
8+
raise EnvironmentError(f"{var_name} environment variable is required")
9+
10+
return value
11+
12+
13+
def get_apigee_username() -> str:
14+
return get_env_var("APIGEE_USERNAME")
15+
16+
17+
def get_proxy_name() -> str:
18+
return get_env_var("PROXY_NAME")
19+
20+
21+
def is_pr_env() -> bool:
22+
"""Checks if the tests are running against a dynamic PR environment"""
23+
proxy_name = get_proxy_name()
24+
return proxy_name.startswith("immunisation-fhir-api-pr-")
25+
26+
27+
def get_apigee_access_token() -> str:
28+
return get_env_var("APIGEE_ACCESS_TOKEN")

0 commit comments

Comments
 (0)