From 223e984f22419695f0914d1b62107dd9527d27ce Mon Sep 17 00:00:00 2001 From: Jack Wainwright <79214177+nhsd-jack-wainwright@users.noreply.github.com> Date: Thu, 23 Apr 2026 09:38:32 +0000 Subject: [PATCH 01/18] [CDAPI-148]: Added initial PDM mock client for integration tests --- pathology-api/tests/conftest.py | 33 +++++++++++++++++++ .../tests/integration/test_endpoints.py | 9 ++++- pathology-api/tests/mock_client.py | 32 ++++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 pathology-api/tests/mock_client.py diff --git a/pathology-api/tests/conftest.py b/pathology-api/tests/conftest.py index 3fde7e72..b0a169ee 100644 --- a/pathology-api/tests/conftest.py +++ b/pathology-api/tests/conftest.py @@ -1,6 +1,8 @@ """Pytest configuration and shared fixtures for pathology API tests.""" import os +import tempfile +from collections.abc import Generator from datetime import timedelta from typing import Any, Literal, Protocol, cast @@ -8,6 +10,8 @@ import requests from dotenv import load_dotenv +from .mock_client import CertificateDetails, PDMMockClient + load_dotenv() type _RequestMethod = Literal["GET", "POST"] @@ -252,6 +256,35 @@ def client(request: pytest.FixtureRequest, base_url: str) -> Client: raise ValueError(f"Unknown env: {env}") +@pytest.fixture(scope="module") +def client_cert() -> Generator[CertificateDetails | None, None, None]: + client_cert = _fetch_env_variable("CLIENT_CERT", str) + client_key = _fetch_env_variable("CLIENT_KEY", str) + with ( + tempfile.NamedTemporaryFile(delete=True) as cert_file, + tempfile.NamedTemporaryFile(delete=True) as key_file, + ): + cert_file.write(client_cert.encode()) + cert_file.flush() + key_file.write(client_key.encode()) + key_file.flush() + yield { + "cert_path": cert_file.name, + "key_path": key_file.name, + } + + yield None + + +@pytest.fixture(scope="module") +def pdm_mock_client(client_cert: CertificateDetails | None) -> PDMMockClient: + return PDMMockClient( + url=_fetch_env_variable("PDM_MOCK_URL", str), + timeout=timedelta(seconds=5), + client_cert=client_cert, + ) + + def _create_remote_client(request: pytest.FixtureRequest) -> RemoteClient: """Create a RemoteClient with auth headers chosen by test markers. diff --git a/pathology-api/tests/integration/test_endpoints.py b/pathology-api/tests/integration/test_endpoints.py index 66141796..c6e3e995 100644 --- a/pathology-api/tests/integration/test_endpoints.py +++ b/pathology-api/tests/integration/test_endpoints.py @@ -12,11 +12,15 @@ from pydantic import BaseModel, HttpUrl from tests.conftest import Client +from tests.mock_client import PDMMockClient class TestBundleEndpoint: def test_bundle_returns_200( - self, client: Client, build_valid_test_result: Callable[[str, str], Bundle] + self, + client: Client, + build_valid_test_result: Callable[[str, str], Bundle], + pdm_mock_client: PDMMockClient, ) -> None: bundle = build_valid_test_result("nhs_number", "ods_code") @@ -53,6 +57,9 @@ def test_bundle_returns_200( assert response.headers["etag"] == 'W/"1"' + sent_request = pdm_mock_client.retrieve_sent_request(response_bundle.id) + assert sent_request == bundle.model_dump_json(by_alias=True) + def test_no_payload_returns_error(self, client: Client) -> None: response = client.send_without_payload( request_method="POST", path="FHIR/R4/Bundle" diff --git a/pathology-api/tests/mock_client.py b/pathology-api/tests/mock_client.py new file mode 100644 index 00000000..c1668f22 --- /dev/null +++ b/pathology-api/tests/mock_client.py @@ -0,0 +1,32 @@ +from datetime import timedelta +from typing import Any, TypedDict + +import requests + + +class CertificateDetails(TypedDict): + cert_path: str + key_path: str + + +class PDMMockClient: + def __init__( + self, url: str, timeout: timedelta, client_cert: CertificateDetails | None + ): + self._url = url + self._timeout = timeout + self._client_cert = client_cert + + def retrieve_sent_request(self, request_id: str) -> Any: + certs = ( + (self._client_cert["cert_path"], self._client_cert["key_path"]) + if self._client_cert + else None + ) + + response = requests.get( + self._url + request_id, + timeout=self._timeout.total_seconds(), + cert=certs, + ) + return response.json() From c6806794080cc1d49f785545d796170b04427700 Mon Sep 17 00:00:00 2001 From: Jack Wainwright <79214177+nhsd-jack-wainwright@users.noreply.github.com> Date: Thu, 23 Apr 2026 10:13:38 +0000 Subject: [PATCH 02/18] [CDAPI-148]: Added initial MNS mock client for use by the integration tests --- pathology-api/tests/conftest.py | 57 +++++++++++++------ .../tests/integration/test_endpoints.py | 24 +++++++- pathology-api/tests/mock_client.py | 25 +++++++- 3 files changed, 87 insertions(+), 19 deletions(-) diff --git a/pathology-api/tests/conftest.py b/pathology-api/tests/conftest.py index b0a169ee..59de7b1f 100644 --- a/pathology-api/tests/conftest.py +++ b/pathology-api/tests/conftest.py @@ -10,7 +10,7 @@ import requests from dotenv import load_dotenv -from .mock_client import CertificateDetails, PDMMockClient +from .mock_client import CertificateDetails, MNSMockClient, PDMMockClient load_dotenv() @@ -260,26 +260,51 @@ def client(request: pytest.FixtureRequest, base_url: str) -> Client: def client_cert() -> Generator[CertificateDetails | None, None, None]: client_cert = _fetch_env_variable("CLIENT_CERT", str) client_key = _fetch_env_variable("CLIENT_KEY", str) - with ( - tempfile.NamedTemporaryFile(delete=True) as cert_file, - tempfile.NamedTemporaryFile(delete=True) as key_file, - ): - cert_file.write(client_cert.encode()) - cert_file.flush() - key_file.write(client_key.encode()) - key_file.flush() - yield { - "cert_path": cert_file.name, - "key_path": key_file.name, - } - yield None + if client_cert and client_key: + with ( + tempfile.NamedTemporaryFile(delete=True) as cert_file, + tempfile.NamedTemporaryFile(delete=True) as key_file, + ): + cert_file.write(client_cert.encode()) + cert_file.flush() + key_file.write(client_key.encode()) + key_file.flush() + yield { + "cert_path": cert_file.name, + "key_path": key_file.name, + } + else: + yield None + + +@pytest.fixture(scope="module") +def pdm_mock_url() -> str: + return _fetch_env_variable("PDM_MOCK_URL", str) + + +@pytest.fixture(scope="module") +def mns_mock_url() -> str: + return _fetch_env_variable("MNS_MOCK_URL", str) @pytest.fixture(scope="module") -def pdm_mock_client(client_cert: CertificateDetails | None) -> PDMMockClient: +def pdm_mock_client( + client_cert: CertificateDetails | None, pdm_mock_url: str +) -> PDMMockClient: return PDMMockClient( - url=_fetch_env_variable("PDM_MOCK_URL", str), + url=pdm_mock_url, + timeout=timedelta(seconds=5), + client_cert=client_cert, + ) + + +@pytest.fixture(scope="module") +def mns_mock_client( + client_cert: CertificateDetails | None, mns_mock_url: str +) -> MNSMockClient: + return MNSMockClient( + url=mns_mock_url, timeout=timedelta(seconds=5), client_cert=client_cert, ) diff --git a/pathology-api/tests/integration/test_endpoints.py b/pathology-api/tests/integration/test_endpoints.py index c6e3e995..21a745d8 100644 --- a/pathology-api/tests/integration/test_endpoints.py +++ b/pathology-api/tests/integration/test_endpoints.py @@ -12,7 +12,7 @@ from pydantic import BaseModel, HttpUrl from tests.conftest import Client -from tests.mock_client import PDMMockClient +from tests.mock_client import MNSMockClient, PDMMockClient class TestBundleEndpoint: @@ -21,8 +21,12 @@ def test_bundle_returns_200( client: Client, build_valid_test_result: Callable[[str, str], Bundle], pdm_mock_client: PDMMockClient, + mns_mock_client: MNSMockClient, + pdm_mock_url: str, ) -> None: - bundle = build_valid_test_result("nhs_number", "ods_code") + subject = "nhs_number" + requesting_ods_code = "ods_code" + bundle = build_valid_test_result(subject, requesting_ods_code) response = client.send( data=bundle.model_dump_json(by_alias=True), @@ -60,6 +64,22 @@ def test_bundle_returns_200( sent_request = pdm_mock_client.retrieve_sent_request(response_bundle.id) assert sent_request == bundle.model_dump_json(by_alias=True) + published_events = mns_mock_client.retrieve_sent_messages(subject) + assert len(published_events) == 1 + + published_event = published_events[0] + assert published_event["subject"] == subject + assert published_event["dataref"] == pdm_mock_url + response_bundle.id + assert published_event["filtering"] == { + "requestingOrganisationODS": requesting_ods_code + } + assert ( + published_event["type"] + == "pathology-laboratory-reporting-test-result-stored-1" + ) + assert published_event["source"] == "uk.nhs.pathology-laboratory-reporting" + assert published_event["specversion"] == "1.0" + def test_no_payload_returns_error(self, client: Client) -> None: response = client.send_without_payload( request_method="POST", path="FHIR/R4/Bundle" diff --git a/pathology-api/tests/mock_client.py b/pathology-api/tests/mock_client.py index c1668f22..0a568d27 100644 --- a/pathology-api/tests/mock_client.py +++ b/pathology-api/tests/mock_client.py @@ -1,5 +1,5 @@ from datetime import timedelta -from typing import Any, TypedDict +from typing import Any, TypedDict, cast import requests @@ -30,3 +30,26 @@ def retrieve_sent_request(self, request_id: str) -> Any: cert=certs, ) return response.json() + + +class MNSMockClient: + def __init__( + self, url: str, timeout: timedelta, client_cert: CertificateDetails | None + ): + self._url = url + self._timeout = timeout + self._client_cert = client_cert + + def retrieve_sent_messages(self, subject: str) -> list[Any]: + certs = ( + (self._client_cert["cert_path"], self._client_cert["key_path"]) + if self._client_cert + else None + ) + + response = requests.get( + self._url + subject, + timeout=self._timeout.total_seconds(), + cert=certs, + ) + return cast("list[Any]", response.json().get("events", [])) From 021dff5e4ad136c69300a977b27699a9413204b6 Mon Sep 17 00:00:00 2001 From: Jack Wainwright <79214177+nhsd-jack-wainwright@users.noreply.github.com> Date: Thu, 23 Apr 2026 10:45:19 +0000 Subject: [PATCH 03/18] [CDAPI-148]: Added new environment variables for PDM and MNS mock clients to integration test action --- .github/actions/run-test-suite/action.yaml | 31 ++++++++++++++++++++++ .github/workflows/preview-env.yaml | 4 +++ pathology-api/tests/conftest.py | 16 +++++------ pathology-api/tests/mock_client.py | 18 ++++++++----- 4 files changed, 55 insertions(+), 14 deletions(-) diff --git a/.github/actions/run-test-suite/action.yaml b/.github/actions/run-test-suite/action.yaml index d84e9ae5..1cbb719f 100644 --- a/.github/actions/run-test-suite/action.yaml +++ b/.github/actions/run-test-suite/action.yaml @@ -17,6 +17,22 @@ inputs: description: "Environment: local or remote" required: false default: "remote" + apim_client_key_secret_name: + description: "Secret name of the APIM client certificate key (if needed)" + required: false + default: "" + apim_client_cert_secret_name: + description: "Secret name of the APIM client certificate (if needed)" + required: false + default: "" + pdm_mock_document_url: + description: "PDM mock document URL (if needed)" + required: false + default: "" + mns_mock_events_url: + description: "MNS mock events URL (if needed)" + required: false + default: "" runs: using: composite @@ -27,10 +43,25 @@ runs: APIGEE_ACCESS_TOKEN: ${{ inputs.apigee-access-token }} ENV: ${{ inputs.env }} TEST_TYPE: ${{ inputs.test-type }} + CLIENT_KEY_NAME: ${{ inputs.apim_client_key_secret_name }} + CLIENT_CERT_NAME: ${{ inputs.apim_client_cert_secret_name }} + PDM_MOCK_DOCUMENT_URL: ${{ inputs.pdm_mock_document_url }} + MNS_MOCK_EVENTS_URL: ${{ inputs.mns_mock_events_url }} run: | if [[ -n "${APIGEE_ACCESS_TOKEN}" ]]; then echo "::add-mask::${APIGEE_ACCESS_TOKEN}" fi + + if [[ -n "${CLIENT_KEY_NAME}" ]]; then + echo "Using APIM client certificate key from name: ${CLIENT_KEY_NAME}" + export CLIENT_KEY=$(printf '%s' "$CLIENT_KEY_NAME") + fi + + if [[ -n "${CLIENT_CERT_NAME}" ]]; then + echo "Using APIM client certificate from name: ${CLIENT_CERT_NAME}" + export CLIENT_CERT=$(printf '%s' "$CLIENT_CERT_NAME") + fi + make test-${TEST_TYPE} - name: "Upload ${{ inputs.test-type }} test results" diff --git a/.github/workflows/preview-env.yaml b/.github/workflows/preview-env.yaml index bc200c3a..ca96b21d 100644 --- a/.github/workflows/preview-env.yaml +++ b/.github/workflows/preview-env.yaml @@ -683,6 +683,10 @@ jobs: with: test-type: integration apigee-access-token: ${{ steps.apigee-token.outputs.apigee-access-token }} + apim_client_key_secret_name: $_cds_pathology_dev_mtls_client1_key_secret + apim_client_cert_secret_name: $_cds_pathology_dev_mtls_client1_key_public + pdm_mock_document_url: ${{ steps.names.outputs.mock_preview_url }}/pdm/mock/Bundle + mns_mock_events_url: ${{ steps.names.outputs.mock_preview_url }}/mns/mock/event - name: "Run acceptance tests" if: github.event.action != 'closed' diff --git a/pathology-api/tests/conftest.py b/pathology-api/tests/conftest.py index 59de7b1f..d1c6b892 100644 --- a/pathology-api/tests/conftest.py +++ b/pathology-api/tests/conftest.py @@ -279,21 +279,21 @@ def client_cert() -> Generator[CertificateDetails | None, None, None]: @pytest.fixture(scope="module") -def pdm_mock_url() -> str: - return _fetch_env_variable("PDM_MOCK_URL", str) +def pdm_mock_document_url() -> str: + return _fetch_env_variable("PDM_MOCK_DOCUMENT_URL", str) @pytest.fixture(scope="module") -def mns_mock_url() -> str: - return _fetch_env_variable("MNS_MOCK_URL", str) +def mns_mock_events_url() -> str: + return _fetch_env_variable("MNS_MOCK_EVENTS_URL", str) @pytest.fixture(scope="module") def pdm_mock_client( - client_cert: CertificateDetails | None, pdm_mock_url: str + client_cert: CertificateDetails | None, pdm_mock_document_url: str ) -> PDMMockClient: return PDMMockClient( - url=pdm_mock_url, + document_url=pdm_mock_document_url, timeout=timedelta(seconds=5), client_cert=client_cert, ) @@ -301,10 +301,10 @@ def pdm_mock_client( @pytest.fixture(scope="module") def mns_mock_client( - client_cert: CertificateDetails | None, mns_mock_url: str + client_cert: CertificateDetails | None, mns_mock_events_url: str ) -> MNSMockClient: return MNSMockClient( - url=mns_mock_url, + events_url=mns_mock_events_url, timeout=timedelta(seconds=5), client_cert=client_cert, ) diff --git a/pathology-api/tests/mock_client.py b/pathology-api/tests/mock_client.py index 0a568d27..743c24b3 100644 --- a/pathology-api/tests/mock_client.py +++ b/pathology-api/tests/mock_client.py @@ -11,9 +11,12 @@ class CertificateDetails(TypedDict): class PDMMockClient: def __init__( - self, url: str, timeout: timedelta, client_cert: CertificateDetails | None + self, + document_url: str, + timeout: timedelta, + client_cert: CertificateDetails | None, ): - self._url = url + self._document_url = document_url self._timeout = timeout self._client_cert = client_cert @@ -25,7 +28,7 @@ def retrieve_sent_request(self, request_id: str) -> Any: ) response = requests.get( - self._url + request_id, + self._document_url + "/" + request_id, timeout=self._timeout.total_seconds(), cert=certs, ) @@ -34,9 +37,12 @@ def retrieve_sent_request(self, request_id: str) -> Any: class MNSMockClient: def __init__( - self, url: str, timeout: timedelta, client_cert: CertificateDetails | None + self, + events_url: str, + timeout: timedelta, + client_cert: CertificateDetails | None, ): - self._url = url + self._events_url = events_url self._timeout = timeout self._client_cert = client_cert @@ -48,7 +54,7 @@ def retrieve_sent_messages(self, subject: str) -> list[Any]: ) response = requests.get( - self._url + subject, + self._events_url + "?subject=" + subject, timeout=self._timeout.total_seconds(), cert=certs, ) From 3112f59fa47079f6f1a4aba3dc25a778515430de Mon Sep 17 00:00:00 2001 From: Jack Wainwright <79214177+nhsd-jack-wainwright@users.noreply.github.com> Date: Thu, 23 Apr 2026 13:37:32 +0000 Subject: [PATCH 04/18] [CDAPI-148]: Moved new mock clients into integration specific conftest file --- pathology-api/tests/conftest.py | 93 +++++---------------- pathology-api/tests/integration/conftest.py | 63 ++++++++++++++ 2 files changed, 86 insertions(+), 70 deletions(-) create mode 100644 pathology-api/tests/integration/conftest.py diff --git a/pathology-api/tests/conftest.py b/pathology-api/tests/conftest.py index d1c6b892..e72ab43a 100644 --- a/pathology-api/tests/conftest.py +++ b/pathology-api/tests/conftest.py @@ -1,17 +1,14 @@ """Pytest configuration and shared fixtures for pathology API tests.""" import os -import tempfile -from collections.abc import Generator +from collections.abc import Callable from datetime import timedelta -from typing import Any, Literal, Protocol, cast +from typing import Any, Literal, Protocol import pytest import requests from dotenv import load_dotenv -from .mock_client import CertificateDetails, MNSMockClient, PDMMockClient - load_dotenv() type _RequestMethod = Literal["GET", "POST"] @@ -213,15 +210,32 @@ def _send( @pytest.fixture(scope="module") -def base_url() -> str: +def fetch_env_variable[T]() -> Callable[[str, type[T]], T]: + def _fetch_env_variable(name: str, required_type: type[T]) -> T: + value = os.getenv(name) + if not value: + raise ValueError(f"{name} environment variable is not set.") + + if not isinstance(value, required_type): + raise ValueError( + f"{name} environment variable is not required type {required_type}" + ) + + return value + + return _fetch_env_variable + + +@pytest.fixture(scope="module") +def base_url(fetch_env_variable: Callable[[str, type[str]], str]) -> str: """Retrieves the base URL of the currently deployed application.""" - return _fetch_env_variable("BASE_URL", str) + return fetch_env_variable("BASE_URL", str) @pytest.fixture -def hostname() -> str: +def hostname(fetch_env_variable: Callable[[str, type[str]], str]) -> str: """Retrieves the hostname of the currently deployed application.""" - return _fetch_env_variable("HOST", str) + return fetch_env_variable("HOST", str) @pytest.fixture @@ -256,60 +270,6 @@ def client(request: pytest.FixtureRequest, base_url: str) -> Client: raise ValueError(f"Unknown env: {env}") -@pytest.fixture(scope="module") -def client_cert() -> Generator[CertificateDetails | None, None, None]: - client_cert = _fetch_env_variable("CLIENT_CERT", str) - client_key = _fetch_env_variable("CLIENT_KEY", str) - - if client_cert and client_key: - with ( - tempfile.NamedTemporaryFile(delete=True) as cert_file, - tempfile.NamedTemporaryFile(delete=True) as key_file, - ): - cert_file.write(client_cert.encode()) - cert_file.flush() - key_file.write(client_key.encode()) - key_file.flush() - yield { - "cert_path": cert_file.name, - "key_path": key_file.name, - } - else: - yield None - - -@pytest.fixture(scope="module") -def pdm_mock_document_url() -> str: - return _fetch_env_variable("PDM_MOCK_DOCUMENT_URL", str) - - -@pytest.fixture(scope="module") -def mns_mock_events_url() -> str: - return _fetch_env_variable("MNS_MOCK_EVENTS_URL", str) - - -@pytest.fixture(scope="module") -def pdm_mock_client( - client_cert: CertificateDetails | None, pdm_mock_document_url: str -) -> PDMMockClient: - return PDMMockClient( - document_url=pdm_mock_document_url, - timeout=timedelta(seconds=5), - client_cert=client_cert, - ) - - -@pytest.fixture(scope="module") -def mns_mock_client( - client_cert: CertificateDetails | None, mns_mock_events_url: str -) -> MNSMockClient: - return MNSMockClient( - events_url=mns_mock_events_url, - timeout=timedelta(seconds=5), - client_cert=client_cert, - ) - - def _create_remote_client(request: pytest.FixtureRequest) -> RemoteClient: """Create a RemoteClient with auth headers chosen by test markers. @@ -338,13 +298,6 @@ def _create_remote_client(request: pytest.FixtureRequest) -> RemoteClient: ) -def _fetch_env_variable[T](name: str, _: type[T]) -> T: - value = os.getenv(name) - if not value: - raise ValueError(f"{name} environment variable is not set.") - return cast("T", value) - - def pytest_addoption(parser: pytest.Parser) -> None: parser.addoption( "--env", diff --git a/pathology-api/tests/integration/conftest.py b/pathology-api/tests/integration/conftest.py new file mode 100644 index 00000000..8dc3aa5d --- /dev/null +++ b/pathology-api/tests/integration/conftest.py @@ -0,0 +1,63 @@ +import tempfile +from collections.abc import Callable, Generator +from datetime import timedelta + +import pytest + +from tests.mock_client import CertificateDetails, MNSMockClient, PDMMockClient + + +@pytest.fixture(scope="module") +def client_cert( + fetch_env_variable: Callable[[str, type[str]], str], +) -> Generator[CertificateDetails | None, None, None]: + client_cert = fetch_env_variable("CLIENT_CERT", str) + client_key = fetch_env_variable("CLIENT_KEY", str) + + if client_cert and client_key: + with ( + tempfile.NamedTemporaryFile(delete=True) as cert_file, + tempfile.NamedTemporaryFile(delete=True) as key_file, + ): + cert_file.write(client_cert.encode()) + cert_file.flush() + key_file.write(client_key.encode()) + key_file.flush() + yield { + "cert_path": cert_file.name, + "key_path": key_file.name, + } + else: + yield None + + +@pytest.fixture(scope="module") +def pdm_mock_document_url(fetch_env_variable: Callable[[str, type[str]], str]) -> str: + return fetch_env_variable("PDM_MOCK_DOCUMENT_URL", str) + + +@pytest.fixture(scope="module") +def mns_mock_events_url(fetch_env_variable: Callable[[str, type[str]], str]) -> str: + return fetch_env_variable("MNS_MOCK_EVENTS_URL", str) + + +@pytest.fixture(scope="module") +def pdm_mock_client( + client_cert: CertificateDetails | None, pdm_mock_document_url: str +) -> PDMMockClient: + return PDMMockClient( + document_url=pdm_mock_document_url, + timeout=timedelta(seconds=5), + client_cert=client_cert, + ) + + +@pytest.fixture(scope="module") +def mns_mock_client( + client_cert: CertificateDetails | None, mns_mock_events_url: str +) -> MNSMockClient: + return MNSMockClient( + events_url=mns_mock_events_url, + timeout=timedelta(seconds=5), + client_cert=client_cert, + ) From 882925a57b791324a7eafc2a76c250aa77040f14 Mon Sep 17 00:00:00 2001 From: Jack Wainwright <79214177+nhsd-jack-wainwright@users.noreply.github.com> Date: Thu, 23 Apr 2026 15:09:07 +0000 Subject: [PATCH 05/18] [CDAPI-148]: Removed exports from `run-test-action` action --- .github/actions/run-test-suite/action.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/run-test-suite/action.yaml b/.github/actions/run-test-suite/action.yaml index 1cbb719f..9f0edabe 100644 --- a/.github/actions/run-test-suite/action.yaml +++ b/.github/actions/run-test-suite/action.yaml @@ -54,12 +54,12 @@ runs: if [[ -n "${CLIENT_KEY_NAME}" ]]; then echo "Using APIM client certificate key from name: ${CLIENT_KEY_NAME}" - export CLIENT_KEY=$(printf '%s' "$CLIENT_KEY_NAME") + CLIENT_KEY=$(printf '%s' "$CLIENT_KEY_NAME") fi if [[ -n "${CLIENT_CERT_NAME}" ]]; then echo "Using APIM client certificate from name: ${CLIENT_CERT_NAME}" - export CLIENT_CERT=$(printf '%s' "$CLIENT_CERT_NAME") + CLIENT_CERT=$(printf '%s' "$CLIENT_CERT_NAME") fi make test-${TEST_TYPE} From f8e01d10a1472b2bec0edcc8c58ade4d0a702e6b Mon Sep 17 00:00:00 2001 From: Jack Wainwright <79214177+nhsd-jack-wainwright@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:31:21 +0000 Subject: [PATCH 06/18] [CDAPI-148]: Added actions for fetching secrets within run-test-suite action --- .github/actions/run-test-suite/action.yaml | 24 ++++++++++++++++++---- .github/workflows/preview-env.yaml | 7 +++++-- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/.github/actions/run-test-suite/action.yaml b/.github/actions/run-test-suite/action.yaml index 9f0edabe..38e662b6 100644 --- a/.github/actions/run-test-suite/action.yaml +++ b/.github/actions/run-test-suite/action.yaml @@ -37,6 +37,20 @@ inputs: runs: using: composite steps: + - name: Fetch Client Key + if: ${{ inputs.apim_client_key_secret_name != '' }} + id: fetch-client-key + uses: aws-actions/aws-secretsmanager-get-secrets@2cb1a461cbd4865ac4299648312e4704c646cd53 + with: + secret-ids: ${{ inputs.apim_client_key_secret_name }} + name-transformation: lowercase + - name: Fetch Client Certificate + if: ${{ inputs.apim_client_cert_secret_name != '' }} + id: fetch-client-cert + uses: aws-actions/aws-secretsmanager-get-secrets@2cb1a461cbd4865ac4299648312e4704c646cd53 + with: + secret-ids: ${{ inputs.apim_client_cert_secret_name }} + name-transformation: lowercase - name: "Run ${{ inputs.test-type }} tests" shell: bash env: @@ -53,13 +67,15 @@ runs: fi if [[ -n "${CLIENT_KEY_NAME}" ]]; then - echo "Using APIM client certificate key from name: ${CLIENT_KEY_NAME}" - CLIENT_KEY=$(printf '%s' "$CLIENT_KEY_NAME") + SECRET_NAME=${CLIENT_KEY_NAME//\//_} + echo "Using APIM client certificate key from name: ${SECRET_NAME}" + CLIENT_KEY=${!SECRET_NAME} fi if [[ -n "${CLIENT_CERT_NAME}" ]]; then - echo "Using APIM client certificate from name: ${CLIENT_CERT_NAME}" - CLIENT_CERT=$(printf '%s' "$CLIENT_CERT_NAME") + SECRET_NAME=${CLIENT_CERT_NAME//\//_} + echo "Using APIM client certificate from name: ${SECRET_NAME}" + CLIENT_CERT=${!SECRET_NAME} fi make test-${TEST_TYPE} diff --git a/.github/workflows/preview-env.yaml b/.github/workflows/preview-env.yaml index ca96b21d..04e193e5 100644 --- a/.github/workflows/preview-env.yaml +++ b/.github/workflows/preview-env.yaml @@ -680,11 +680,14 @@ jobs: - name: "Run integration tests" if: github.event.action != 'closed' uses: ./.github/actions/run-test-suite + env: + API_MTLS_CERT: ${{ secrets.API_MTLS_CERT }} + API_MTLS_KEY: ${{ secrets.API_MTLS_KEY }} with: test-type: integration apigee-access-token: ${{ steps.apigee-token.outputs.apigee-access-token }} - apim_client_key_secret_name: $_cds_pathology_dev_mtls_client1_key_secret - apim_client_cert_secret_name: $_cds_pathology_dev_mtls_client1_key_public + apim_client_key_secret_name: "${{ env.API_MTLS_CERT || '/cds/pathology/dev/mtls/client1-key-public' }}" + apim_client_cert_secret_name: "${{ env.API_MTLS_KEY || '/cds/pathology/dev/mtls/client1-key-secret' }}" pdm_mock_document_url: ${{ steps.names.outputs.mock_preview_url }}/pdm/mock/Bundle mns_mock_events_url: ${{ steps.names.outputs.mock_preview_url }}/mns/mock/event From 0a8f1b2ad9c8cce81e153c73b1e84a7f5173aff6 Mon Sep 17 00:00:00 2001 From: Jack Wainwright <79214177+nhsd-jack-wainwright@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:43:11 +0000 Subject: [PATCH 07/18] [CDAPI-148]: Removed unnecessary secret retrieval from run-test-suite action --- .github/actions/run-test-suite/action.yaml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/.github/actions/run-test-suite/action.yaml b/.github/actions/run-test-suite/action.yaml index 38e662b6..cf2e8c41 100644 --- a/.github/actions/run-test-suite/action.yaml +++ b/.github/actions/run-test-suite/action.yaml @@ -37,20 +37,6 @@ inputs: runs: using: composite steps: - - name: Fetch Client Key - if: ${{ inputs.apim_client_key_secret_name != '' }} - id: fetch-client-key - uses: aws-actions/aws-secretsmanager-get-secrets@2cb1a461cbd4865ac4299648312e4704c646cd53 - with: - secret-ids: ${{ inputs.apim_client_key_secret_name }} - name-transformation: lowercase - - name: Fetch Client Certificate - if: ${{ inputs.apim_client_cert_secret_name != '' }} - id: fetch-client-cert - uses: aws-actions/aws-secretsmanager-get-secrets@2cb1a461cbd4865ac4299648312e4704c646cd53 - with: - secret-ids: ${{ inputs.apim_client_cert_secret_name }} - name-transformation: lowercase - name: "Run ${{ inputs.test-type }} tests" shell: bash env: From 64f629a1d3075200e3261362f55f431daf59a9e4 Mon Sep 17 00:00:00 2001 From: Jack Wainwright <79214177+nhsd-jack-wainwright@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:08:30 +0000 Subject: [PATCH 08/18] [CDAPI-148]: Added in replacements for dashes within secret names --- .github/actions/run-test-suite/action.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/actions/run-test-suite/action.yaml b/.github/actions/run-test-suite/action.yaml index cf2e8c41..9d745963 100644 --- a/.github/actions/run-test-suite/action.yaml +++ b/.github/actions/run-test-suite/action.yaml @@ -54,12 +54,14 @@ runs: if [[ -n "${CLIENT_KEY_NAME}" ]]; then SECRET_NAME=${CLIENT_KEY_NAME//\//_} + SECRET_NAME=${SECRET_NAME//-/_} echo "Using APIM client certificate key from name: ${SECRET_NAME}" CLIENT_KEY=${!SECRET_NAME} fi if [[ -n "${CLIENT_CERT_NAME}" ]]; then SECRET_NAME=${CLIENT_CERT_NAME//\//_} + SECRET_NAME=${SECRET_NAME//-/_} echo "Using APIM client certificate from name: ${SECRET_NAME}" CLIENT_CERT=${!SECRET_NAME} fi From 0c383bdb7354fd9d08cc44877fa3ef3b541ee238 Mon Sep 17 00:00:00 2001 From: Jack Wainwright <79214177+nhsd-jack-wainwright@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:20:01 +0000 Subject: [PATCH 09/18] [CDAPI-148]: Added environment variables to make test command --- .github/actions/run-test-suite/action.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/run-test-suite/action.yaml b/.github/actions/run-test-suite/action.yaml index 9d745963..38c7466e 100644 --- a/.github/actions/run-test-suite/action.yaml +++ b/.github/actions/run-test-suite/action.yaml @@ -66,7 +66,7 @@ runs: CLIENT_CERT=${!SECRET_NAME} fi - make test-${TEST_TYPE} + CLIENT_KEY=${CLIENT_KEY} CLIENT_CERT=${CLIENT_CERT} make test-${TEST_TYPE} - name: "Upload ${{ inputs.test-type }} test results" if: always() From 1976ec3ebafa71d8b45b82f01bcd318badcfbf6f Mon Sep 17 00:00:00 2001 From: Jack Wainwright <79214177+nhsd-jack-wainwright@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:43:15 +0000 Subject: [PATCH 10/18] [CDAPI-148]: Fixed fixture name within integration test --- pathology-api/tests/integration/test_endpoints.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pathology-api/tests/integration/test_endpoints.py b/pathology-api/tests/integration/test_endpoints.py index 21a745d8..db4eb17f 100644 --- a/pathology-api/tests/integration/test_endpoints.py +++ b/pathology-api/tests/integration/test_endpoints.py @@ -22,7 +22,7 @@ def test_bundle_returns_200( build_valid_test_result: Callable[[str, str], Bundle], pdm_mock_client: PDMMockClient, mns_mock_client: MNSMockClient, - pdm_mock_url: str, + pdm_mock_document_url: str, ) -> None: subject = "nhs_number" requesting_ods_code = "ods_code" @@ -69,7 +69,7 @@ def test_bundle_returns_200( published_event = published_events[0] assert published_event["subject"] == subject - assert published_event["dataref"] == pdm_mock_url + response_bundle.id + assert published_event["dataref"] == pdm_mock_document_url + response_bundle.id assert published_event["filtering"] == { "requestingOrganisationODS": requesting_ods_code } From 2ef3f5fff2601e33cc8cf45d4b3adeb512485308 Mon Sep 17 00:00:00 2001 From: Jack Wainwright <79214177+nhsd-jack-wainwright@users.noreply.github.com> Date: Mon, 27 Apr 2026 13:50:41 +0000 Subject: [PATCH 11/18] [CDAPI-148]: Swapped around mTLS key and cert parameter --- .github/workflows/preview-env.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/preview-env.yaml b/.github/workflows/preview-env.yaml index 04e193e5..1a3929bf 100644 --- a/.github/workflows/preview-env.yaml +++ b/.github/workflows/preview-env.yaml @@ -686,8 +686,8 @@ jobs: with: test-type: integration apigee-access-token: ${{ steps.apigee-token.outputs.apigee-access-token }} - apim_client_key_secret_name: "${{ env.API_MTLS_CERT || '/cds/pathology/dev/mtls/client1-key-public' }}" - apim_client_cert_secret_name: "${{ env.API_MTLS_KEY || '/cds/pathology/dev/mtls/client1-key-secret' }}" + apim_client_cert_secret_name: "${{ env.API_MTLS_CERT || '/cds/pathology/dev/mtls/client1-key-public' }}" + apim_client_key_secret_name: "${{ env.API_MTLS_KEY || '/cds/pathology/dev/mtls/client1-key-secret' }}" pdm_mock_document_url: ${{ steps.names.outputs.mock_preview_url }}/pdm/mock/Bundle mns_mock_events_url: ${{ steps.names.outputs.mock_preview_url }}/mns/mock/event From 0a042746cb9b7efc8638f90347a78318a9abba54 Mon Sep 17 00:00:00 2001 From: Jack Wainwright <79214177+nhsd-jack-wainwright@users.noreply.github.com> Date: Mon, 27 Apr 2026 14:34:59 +0000 Subject: [PATCH 12/18] [CDAPI-148]: Updated PDM mock to store received document instead of created document --- mocks/src/pdm_mock/handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mocks/src/pdm_mock/handler.py b/mocks/src/pdm_mock/handler.py index b14945f3..52be4cdf 100644 --- a/mocks/src/pdm_mock/handler.py +++ b/mocks/src/pdm_mock/handler.py @@ -114,7 +114,7 @@ def handle_post_request(payload: dict[str, Any]) -> PDMResponse: item: DocumentItem = { "sessionId": document_id, "expiresAt": int(time()) + 600, - "document": json.dumps(created_document), + "document": json.dumps(payload), "type": "pdm_document", } From 68c980bca59e723bcd249bda52035f27e8cd8d42 Mon Sep 17 00:00:00 2001 From: Jack Wainwright <79214177+nhsd-jack-wainwright@users.noreply.github.com> Date: Mon, 27 Apr 2026 14:54:59 +0000 Subject: [PATCH 13/18] [CDAPI-148]: Dumped retrieved PDM request as a JSON string for comparison --- pathology-api/tests/integration/test_endpoints.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pathology-api/tests/integration/test_endpoints.py b/pathology-api/tests/integration/test_endpoints.py index db4eb17f..833d2416 100644 --- a/pathology-api/tests/integration/test_endpoints.py +++ b/pathology-api/tests/integration/test_endpoints.py @@ -61,7 +61,9 @@ def test_bundle_returns_200( assert response.headers["etag"] == 'W/"1"' - sent_request = pdm_mock_client.retrieve_sent_request(response_bundle.id) + sent_request = json.dumps( + pdm_mock_client.retrieve_sent_request(response_bundle.id) + ) assert sent_request == bundle.model_dump_json(by_alias=True) published_events = mns_mock_client.retrieve_sent_messages(subject) From d12073b5a6d8113aa944f456f9a19cf5042ba28f Mon Sep 17 00:00:00 2001 From: Jack Wainwright <79214177+nhsd-jack-wainwright@users.noreply.github.com> Date: Mon, 27 Apr 2026 16:18:33 +0000 Subject: [PATCH 14/18] [CDAPI-148]: Swapped assertion within test_endpoints.py to compare dicts instead of JSON strings --- pathology-api/tests/integration/test_endpoints.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pathology-api/tests/integration/test_endpoints.py b/pathology-api/tests/integration/test_endpoints.py index 833d2416..a17da7de 100644 --- a/pathology-api/tests/integration/test_endpoints.py +++ b/pathology-api/tests/integration/test_endpoints.py @@ -61,10 +61,8 @@ def test_bundle_returns_200( assert response.headers["etag"] == 'W/"1"' - sent_request = json.dumps( - pdm_mock_client.retrieve_sent_request(response_bundle.id) - ) - assert sent_request == bundle.model_dump_json(by_alias=True) + sent_request = pdm_mock_client.retrieve_sent_request(response_bundle.id) + assert sent_request == bundle.model_dump(by_alias=True) published_events = mns_mock_client.retrieve_sent_messages(subject) assert len(published_events) == 1 From 6394b5554ae1cc9788521d5a640d9af8ef127a41 Mon Sep 17 00:00:00 2001 From: Jack Wainwright <79214177+nhsd-jack-wainwright@users.noreply.github.com> Date: Mon, 27 Apr 2026 16:34:03 +0000 Subject: [PATCH 15/18] [CDAPI-148]: Excluded None values from sent request comparison --- pathology-api/tests/integration/test_endpoints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pathology-api/tests/integration/test_endpoints.py b/pathology-api/tests/integration/test_endpoints.py index a17da7de..2bcc39f5 100644 --- a/pathology-api/tests/integration/test_endpoints.py +++ b/pathology-api/tests/integration/test_endpoints.py @@ -62,7 +62,7 @@ def test_bundle_returns_200( assert response.headers["etag"] == 'W/"1"' sent_request = pdm_mock_client.retrieve_sent_request(response_bundle.id) - assert sent_request == bundle.model_dump(by_alias=True) + assert sent_request == bundle.model_dump(by_alias=True, exclude_none=True) published_events = mns_mock_client.retrieve_sent_messages(subject) assert len(published_events) == 1 From 47ec114c7efd3bc309139ba67b5c6be27d5e3b06 Mon Sep 17 00:00:00 2001 From: Jack Wainwright <79214177+nhsd-jack-wainwright@users.noreply.github.com> Date: Mon, 27 Apr 2026 16:46:59 +0000 Subject: [PATCH 16/18] [CDAPI-148]: Using unique subject identifier within integration test to avoid conflicts --- pathology-api/tests/integration/test_endpoints.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pathology-api/tests/integration/test_endpoints.py b/pathology-api/tests/integration/test_endpoints.py index 2bcc39f5..d7c2dc01 100644 --- a/pathology-api/tests/integration/test_endpoints.py +++ b/pathology-api/tests/integration/test_endpoints.py @@ -1,6 +1,7 @@ """Integration tests for the pathology API using pytest.""" import json +import uuid from collections.abc import Callable from typing import Any, Literal @@ -24,7 +25,7 @@ def test_bundle_returns_200( mns_mock_client: MNSMockClient, pdm_mock_document_url: str, ) -> None: - subject = "nhs_number" + subject = "subject-" + str(uuid.uuid4()) requesting_ods_code = "ods_code" bundle = build_valid_test_result(subject, requesting_ods_code) From b9f2b8caaeadd911ad38ee3ef3c6c75da053970e Mon Sep 17 00:00:00 2001 From: Jack Wainwright <79214177+nhsd-jack-wainwright@users.noreply.github.com> Date: Mon, 27 Apr 2026 17:00:54 +0000 Subject: [PATCH 17/18] [CDAPI-148]: Added PDM bundle URL to test suite action and integration tests --- .github/actions/run-test-suite/action.yaml | 5 +++++ .github/workflows/preview-env.yaml | 1 + pathology-api/tests/integration/conftest.py | 5 +++++ pathology-api/tests/integration/test_endpoints.py | 4 ++-- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/actions/run-test-suite/action.yaml b/.github/actions/run-test-suite/action.yaml index 38c7466e..a1eac393 100644 --- a/.github/actions/run-test-suite/action.yaml +++ b/.github/actions/run-test-suite/action.yaml @@ -33,6 +33,10 @@ inputs: description: "MNS mock events URL (if needed)" required: false default: "" + pdm_bundle_url: + description: "PDM bundle URL (if needed)" + required: false + default: "" runs: using: composite @@ -47,6 +51,7 @@ runs: CLIENT_CERT_NAME: ${{ inputs.apim_client_cert_secret_name }} PDM_MOCK_DOCUMENT_URL: ${{ inputs.pdm_mock_document_url }} MNS_MOCK_EVENTS_URL: ${{ inputs.mns_mock_events_url }} + PDM_BUNDLE_URL: ${{ inputs.pdm_bundle_url }} run: | if [[ -n "${APIGEE_ACCESS_TOKEN}" ]]; then echo "::add-mask::${APIGEE_ACCESS_TOKEN}" diff --git a/.github/workflows/preview-env.yaml b/.github/workflows/preview-env.yaml index 1a3929bf..9fe0bb84 100644 --- a/.github/workflows/preview-env.yaml +++ b/.github/workflows/preview-env.yaml @@ -690,6 +690,7 @@ jobs: apim_client_key_secret_name: "${{ env.API_MTLS_KEY || '/cds/pathology/dev/mtls/client1-key-secret' }}" pdm_mock_document_url: ${{ steps.names.outputs.mock_preview_url }}/pdm/mock/Bundle mns_mock_events_url: ${{ steps.names.outputs.mock_preview_url }}/mns/mock/event + pdm_bundle_url: ${{ steps.names.outputs.int_preview_url }}/pdm/FHIR/R4/Bundle - name: "Run acceptance tests" if: github.event.action != 'closed' diff --git a/pathology-api/tests/integration/conftest.py b/pathology-api/tests/integration/conftest.py index 8dc3aa5d..737e29b3 100644 --- a/pathology-api/tests/integration/conftest.py +++ b/pathology-api/tests/integration/conftest.py @@ -41,6 +41,11 @@ def mns_mock_events_url(fetch_env_variable: Callable[[str, type[str]], str]) -> return fetch_env_variable("MNS_MOCK_EVENTS_URL", str) +@pytest.fixture(scope="module") +def pdm_bundle_url(fetch_env_variable: Callable[[str, type[str]], str]) -> str: + return fetch_env_variable("PDM_BUNDLE_URL", str) + + @pytest.fixture(scope="module") def pdm_mock_client( client_cert: CertificateDetails | None, pdm_mock_document_url: str diff --git a/pathology-api/tests/integration/test_endpoints.py b/pathology-api/tests/integration/test_endpoints.py index d7c2dc01..08d4c4c6 100644 --- a/pathology-api/tests/integration/test_endpoints.py +++ b/pathology-api/tests/integration/test_endpoints.py @@ -23,7 +23,7 @@ def test_bundle_returns_200( build_valid_test_result: Callable[[str, str], Bundle], pdm_mock_client: PDMMockClient, mns_mock_client: MNSMockClient, - pdm_mock_document_url: str, + pdm_bundle_url: str, ) -> None: subject = "subject-" + str(uuid.uuid4()) requesting_ods_code = "ods_code" @@ -70,7 +70,7 @@ def test_bundle_returns_200( published_event = published_events[0] assert published_event["subject"] == subject - assert published_event["dataref"] == pdm_mock_document_url + response_bundle.id + assert published_event["dataref"] == pdm_bundle_url + "/" + response_bundle.id assert published_event["filtering"] == { "requestingOrganisationODS": requesting_ods_code } From cf36e0da7cda33eec41bfbc6fee2b70668b2b56b Mon Sep 17 00:00:00 2001 From: Jack Wainwright <79214177+nhsd-jack-wainwright@users.noreply.github.com> Date: Mon, 27 Apr 2026 17:13:12 +0000 Subject: [PATCH 18/18] [CDAPI-148]: Fixed supplied Bundle URL --- .github/workflows/preview-env.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview-env.yaml b/.github/workflows/preview-env.yaml index 9fe0bb84..3257de25 100644 --- a/.github/workflows/preview-env.yaml +++ b/.github/workflows/preview-env.yaml @@ -690,7 +690,7 @@ jobs: apim_client_key_secret_name: "${{ env.API_MTLS_KEY || '/cds/pathology/dev/mtls/client1-key-secret' }}" pdm_mock_document_url: ${{ steps.names.outputs.mock_preview_url }}/pdm/mock/Bundle mns_mock_events_url: ${{ steps.names.outputs.mock_preview_url }}/mns/mock/event - pdm_bundle_url: ${{ steps.names.outputs.int_preview_url }}/pdm/FHIR/R4/Bundle + pdm_bundle_url: ${{ steps.names.outputs.mock_preview_url }}/pdm/FHIR/R4/Bundle - name: "Run acceptance tests" if: github.event.action != 'closed'