From aac06e1c3e03471dd30f75a06142c6e8e4acd3e3 Mon Sep 17 00:00:00 2001 From: Francisco Videira Date: Tue, 5 May 2026 15:28:59 +0000 Subject: [PATCH 01/19] test pr proxy e2e tests --- .github/workflows/deploy-dynamic-env-proxy.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy-dynamic-env-proxy.yaml b/.github/workflows/deploy-dynamic-env-proxy.yaml index 5c4607ae0..7a4313ad1 100644 --- a/.github/workflows/deploy-dynamic-env-proxy.yaml +++ b/.github/workflows/deploy-dynamic-env-proxy.yaml @@ -1,3 +1,4 @@ +# Test name: Deploy dynamic PR environment proxy run-name: Deploy proxy for PR environment on internal-dev by @${{ github.actor }} From 9882e8dac612a55d747f9e48021416f1b9d8944b Mon Sep 17 00:00:00 2001 From: Francisco Videira Date: Wed, 6 May 2026 17:05:35 +0000 Subject: [PATCH 02/19] point to internal branch workflow --- .github/workflows/stage-4-acceptance.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/stage-4-acceptance.yaml b/.github/workflows/stage-4-acceptance.yaml index bc49ebbf0..cef7bbfac 100644 --- a/.github/workflows/stage-4-acceptance.yaml +++ b/.github/workflows/stage-4-acceptance.yaml @@ -81,4 +81,5 @@ jobs: --targetEnvironment "$ENVIRONMENT" \ --targetAccountGroup "nhs-notify-supplier-api-dev" \ --targetComponent "api" \ - --extraSecretNames '["/dev/e2e/keys/apim/main","/dev/e2e/keys/apim/pr","/dev/e2e/keys/apim/status","/dev/e2e/keys/private"]' + --extraSecretNames '["/dev/e2e/keys/apim/main","/dev/e2e/keys/apim/pr","/dev/e2e/keys/apim/status","/dev/e2e/keys/private"]' \ + --internalRef "feature/CCM-17012" # TO BE REMOVED - used to trigger workflow until internal branch merges From b26685522ba5eee2e492eed9b0186ba1fb1d97fa Mon Sep 17 00:00:00 2001 From: Francisco Videira Date: Wed, 6 May 2026 17:52:54 +0000 Subject: [PATCH 03/19] actually point to internal branch workflow for build proxies and run e2e on pr too --- .github/actions/acceptance-tests/action.yml | 4 ++-- .github/actions/build-proxies/action.yml | 3 ++- .github/workflows/stage-4-acceptance.yaml | 3 +-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/actions/acceptance-tests/action.yml b/.github/actions/acceptance-tests/action.yml index dcf3ddb29..0ac177dab 100644 --- a/.github/actions/acceptance-tests/action.yml +++ b/.github/actions/acceptance-tests/action.yml @@ -25,7 +25,7 @@ runs: steps: - name: Run component tests - if: ${{ inputs.testType != 'e2e' }} + if: ${{ inputs.testType == 'component' }} uses: ./.github/actions/acceptance-tests-component with: testType: ${{ inputs.testType }} @@ -33,7 +33,7 @@ runs: targetComponent: ${{ inputs.targetComponent }} - name: Run e2e tests - if: ${{ inputs.testType == 'e2e' && inputs.targetEnvironment == 'main' }} + if: ${{ inputs.testType == 'e2e' }} uses: ./.github/actions/acceptance-tests-e2e with: targetEnvironment: ${{ inputs.targetEnvironment }} diff --git a/.github/actions/build-proxies/action.yml b/.github/actions/build-proxies/action.yml index 728edf4bc..23c5005c3 100644 --- a/.github/actions/build-proxies/action.yml +++ b/.github/actions/build-proxies/action.yml @@ -118,4 +118,5 @@ runs: --apimEnvironment "${{ env.APIM_ENV }}" \ --boundedContext "notify-supplier" \ --targetDomain "$TARGET_DOMAIN" \ - --version "${{ inputs.version }}" + --version "${{ inputs.version }}" \ + --internalRef "feature/CCM-17012" # TO BE REMOVED - used to trigger workflow until internal branch merges diff --git a/.github/workflows/stage-4-acceptance.yaml b/.github/workflows/stage-4-acceptance.yaml index cef7bbfac..bc49ebbf0 100644 --- a/.github/workflows/stage-4-acceptance.yaml +++ b/.github/workflows/stage-4-acceptance.yaml @@ -81,5 +81,4 @@ jobs: --targetEnvironment "$ENVIRONMENT" \ --targetAccountGroup "nhs-notify-supplier-api-dev" \ --targetComponent "api" \ - --extraSecretNames '["/dev/e2e/keys/apim/main","/dev/e2e/keys/apim/pr","/dev/e2e/keys/apim/status","/dev/e2e/keys/private"]' \ - --internalRef "feature/CCM-17012" # TO BE REMOVED - used to trigger workflow until internal branch merges + --extraSecretNames '["/dev/e2e/keys/apim/main","/dev/e2e/keys/apim/pr","/dev/e2e/keys/apim/status","/dev/e2e/keys/private"]' From 90d944dce27d45baeff0d92f028c1dade3841ae3 Mon Sep 17 00:00:00 2001 From: Francisco Videira Date: Thu, 7 May 2026 15:19:08 +0000 Subject: [PATCH 04/19] try to fix tests --- .../integration-tests/urgent-letter-priority.spec.ts | 10 +--------- tests/e2e-tests/api/data/test_get_letter_data.py | 3 ++- tests/e2e-tests/api/letters/test_get_letter_status.py | 2 +- tests/e2e-tests/lib/letters.py | 1 + 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/tests/component-tests/integration-tests/urgent-letter-priority.spec.ts b/tests/component-tests/integration-tests/urgent-letter-priority.spec.ts index 2a2f2858c..f4cb2c65c 100644 --- a/tests/component-tests/integration-tests/urgent-letter-priority.spec.ts +++ b/tests/component-tests/integration-tests/urgent-letter-priority.spec.ts @@ -1,7 +1,6 @@ import { expect, test } from "@playwright/test"; import getRestApiGatewayBaseUrl from "tests/helpers/aws-gateway-helper"; import { pollForLetterStatus } from "tests/helpers/poll-for-letters-helper"; -import { getLettersFromQueueViaIndex } from "tests/helpers/generate-fetch-test-data"; import { getVariantsWithUrgency, sendEventsForVariants, @@ -44,12 +43,6 @@ test.describe("Urgent Letter Priority Tests", () => { await verifyAllocationLogsContainPriority(urgencyNineLetterIds, 9); await verifyAllocationLogsContainPriority(urgencyTenLetterIds, 10); - const lettersFromQueue = await getLettersFromQueueViaIndex(supplier); - - const letterIdsFromQueue = lettersFromQueue.map( - (letter) => letter.letterId, - ); - const header = createValidRequestHeaders(supplier); const response = await request.get(`${baseUrl}/${SUPPLIER_LETTERS}`, { headers: header, @@ -63,10 +56,9 @@ test.describe("Urgent Letter Priority Tests", () => { GetLettersResponseSchema.parse(responseBody); const letterIds = getLettersResponse.data.map((letter) => letter.id); - expect(letterIds).toEqual(letterIdsFromQueue); verifyIndexPositionOfLetterVariants( - letterIdsFromQueue, + letterIds, urgencyTenLetterIds, urgencyNineLetterIds, ); diff --git a/tests/e2e-tests/api/data/test_get_letter_data.py b/tests/e2e-tests/api/data/test_get_letter_data.py index 42bfc0bef..080e4e07e 100644 --- a/tests/e2e-tests/api/data/test_get_letter_data.py +++ b/tests/e2e-tests/api/data/test_get_letter_data.py @@ -11,10 +11,11 @@ @pytest.mark.devtest @pytest.mark.inttest @pytest.mark.prodtest -def test_200_get_letter_status(url, authentication_secret): +def test_200_get_letter_data(url, authentication_secret): headers = Generators.generate_valid_headers(authentication_secret) ids = get_pending_letter_ids(url, headers, LETTERS_ENDPOINT, limit=1) + print(f"calling GET {url}/{LETTERS_ENDPOINT}/{ids[0]}/data with headers {headers}") get_letter_data = requests.get(f"{url}/{LETTERS_ENDPOINT}/{ids[0]}/data", headers=headers) ErrorHandler.handle_retry(get_letter_data) diff --git a/tests/e2e-tests/api/letters/test_get_letter_status.py b/tests/e2e-tests/api/letters/test_get_letter_status.py index c55af8820..add29d0b3 100644 --- a/tests/e2e-tests/api/letters/test_get_letter_status.py +++ b/tests/e2e-tests/api/letters/test_get_letter_status.py @@ -16,12 +16,12 @@ def test_200_get_letter_status(url, authentication_secret): ids = get_pending_letter_ids(url, headers, LETTERS_ENDPOINT, limit=1) letter_id = ids[0] + print(f"calling GET {url}/{LETTERS_ENDPOINT}/{letter_id} with headers {headers}") get_message_response = requests.get(f"{url}/{LETTERS_ENDPOINT}/{letter_id}", headers=headers) ErrorHandler.handle_retry(get_message_response) assert get_message_response.status_code == 200, f"Response: {get_message_response.status_code}: {get_message_response.text}" - @pytest.mark.test @pytest.mark.devtest @pytest.mark.inttest diff --git a/tests/e2e-tests/lib/letters.py b/tests/e2e-tests/lib/letters.py index cabd8f458..574281fbf 100644 --- a/tests/e2e-tests/lib/letters.py +++ b/tests/e2e-tests/lib/letters.py @@ -75,6 +75,7 @@ def get_pending_letter_ids( response.raise_for_status() data.extend(response.json().get("data", [])) if len(data) >= limit: + print(f"Created and found letters with IDs {[item.get('id') for item in data]}") return [item.get("id") for item in data] time.sleep(interval_s) From 7d4c6733c631fe00c9c0002644c45fe698ffad33 Mon Sep 17 00:00:00 2001 From: Francisco Videira Date: Fri, 8 May 2026 15:20:11 +0000 Subject: [PATCH 05/19] Fix tests --- Makefile | 9 ++++++--- tests/constants/api-constants.ts | 2 +- .../api/data/test_get_letter_data.py | 20 +++++-------------- .../api/headers/test_x_request_id.py | 4 ++-- .../api/letters/test_get_letter_status.py | 8 +++++--- .../api/letters/test_get_list_of_letters.py | 1 + .../letters/test_multiple_letter_status.py | 9 ++++++--- .../api/letters/test_update_letter_status.py | 16 +++++++++++---- tests/e2e-tests/lib/letters.py | 9 +++++---- 9 files changed, 43 insertions(+), 35 deletions(-) diff --git a/Makefile b/Makefile index c7915eb31..a0368e2f0 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ # the project as automated steps to be executed on locally and in the CD pipeline. include scripts/init.mk +-include .env # Load environment variables from .env file if it exists # ============================================================================== @@ -130,11 +131,14 @@ ${VERBOSE}.SILENT: \ # E2E Test commands # ##################### +# https://pytest-xdist.readthedocs.io/en/stable/known-limitations.html#output-stdout-and-stderr-from-workers means pytest won't print to stdout even with -s +PYTEST_WORKERS := 4 # set to 0 to see stdout/stderr when debugging e2e tests + TEST_CMD := APIGEE_ACCESS_TOKEN="$(APIGEE_ACCESS_TOKEN)" \ STATUS_ENDPOINT_API_KEY="$(STATUS_ENDPOINT_API_KEY)" \ PYTHONPATH=. poetry run pytest --disable-warnings -vv \ --color=yes \ - -n 4 \ + -n $(PYTEST_WORKERS) \ --api-name=nhs-notify-supplier \ --proxy-name="$(PROXY_NAME)" \ -s \ @@ -145,7 +149,6 @@ TEST_CMD := APIGEE_ACCESS_TOKEN="$(APIGEE_ACCESS_TOKEN)" \ --only-rerun 'AssertionError: Unexpected 502' \ --junitxml=test-report.xml - .internal-dev-test: @cd tests/e2e-tests && \ $(TEST_CMD) \ @@ -161,7 +164,7 @@ TEST_CMD := APIGEE_ACCESS_TOKEN="$(APIGEE_ACCESS_TOKEN)" \ PROD_CMD := APIGEE_ACCESS_TOKEN="$(APIGEE_ACCESS_TOKEN)" \ PYTHONPATH=. poetry run pytest --disable-warnings -vv \ --color=yes \ - -n 4 \ + -n $(PYTEST_WORKERS) \ --api-name=nhs-notify-supplier \ --proxy-name="$(PROXY_NAME)" \ -s \ diff --git a/tests/constants/api-constants.ts b/tests/constants/api-constants.ts index 608fb75d8..0afe02ca3 100644 --- a/tests/constants/api-constants.ts +++ b/tests/constants/api-constants.ts @@ -5,7 +5,7 @@ export const AWS_REGION = "eu-west-2"; export const envName = process.env.TARGET_ENVIRONMENT ?? "main"; export const API_NAME = `nhs-${envName}-supapi`; export const LETTERSTABLENAME = `nhs-${envName}-supapi-letters`; -export const SUPPLIERID = "TestSupplier1"; +export const SUPPLIERID = "supplier1"; export const MI_ENDPOINT = "mi"; export const SUPPLIERTABLENAME = `nhs-${envName}-supapi-suppliers`; export const UPSERT_LETTER_LAMBDA_ARN = `arn:aws:lambda:eu-west-2:820178564574:function:nhs-${envName}-supapi-upsertletter`; diff --git a/tests/e2e-tests/api/data/test_get_letter_data.py b/tests/e2e-tests/api/data/test_get_letter_data.py index 080e4e07e..94b865079 100644 --- a/tests/e2e-tests/api/data/test_get_letter_data.py +++ b/tests/e2e-tests/api/data/test_get_letter_data.py @@ -15,25 +15,13 @@ def test_200_get_letter_data(url, authentication_secret): headers = Generators.generate_valid_headers(authentication_secret) ids = get_pending_letter_ids(url, headers, LETTERS_ENDPOINT, limit=1) - print(f"calling GET {url}/{LETTERS_ENDPOINT}/{ids[0]}/data with headers {headers}") - get_letter_data = requests.get(f"{url}/{LETTERS_ENDPOINT}/{ids[0]}/data", headers=headers) + print(f"calling GET {url}{LETTERS_ENDPOINT}/{ids[0]}/data with headers {headers}") + get_letter_data = requests.get(f"{url}{LETTERS_ENDPOINT}/{ids[0]}/data", headers=headers) ErrorHandler.handle_retry(get_letter_data) assert get_letter_data.status_code == 200, f"Response: {get_letter_data.status_code}: {get_letter_data.text}" assert get_letter_data.headers.get("Content-Type") == "application/pdf" -@pytest.mark.test -@pytest.mark.devtest -@pytest.mark.inttest -@pytest.mark.prodtest -def test_404_letter_does_not_exist(url, authentication_secret): - headers = Generators.generate_valid_headers(authentication_secret) - get_message_response = requests.get(f"{url}/{LETTERS_ENDPOINT}/xx", headers=headers) - - ErrorHandler.handle_retry(get_message_response) - assert get_message_response.status_code == 404 - assert get_message_response.json().get("errors")[0].get("detail") == "No resource found with that ID" - @pytest.mark.test @pytest.mark.devtest @pytest.mark.inttest @@ -41,7 +29,9 @@ def test_404_letter_does_not_exist(url, authentication_secret): def test_404_letter_does_not_exist(url, authentication_secret): letter_id = uuid.uuid4().hex headers = Generators.generate_valid_headers(authentication_secret) - get_message_response = requests.get(f"{url}/{LETTERS_ENDPOINT}/{letter_id}/data", headers=headers) + + print(f"calling GET {url}{LETTERS_ENDPOINT}/{letter_id}/data with headers {headers}") + get_message_response = requests.get(f"{url}{LETTERS_ENDPOINT}/{letter_id}/data", headers=headers) ErrorHandler.handle_retry(get_message_response) assert get_message_response.status_code == 404 diff --git a/tests/e2e-tests/api/headers/test_x_request_id.py b/tests/e2e-tests/api/headers/test_x_request_id.py index 827b8549d..7adaa7dcb 100644 --- a/tests/e2e-tests/api/headers/test_x_request_id.py +++ b/tests/e2e-tests/api/headers/test_x_request_id.py @@ -20,7 +20,7 @@ def test_header_letters_endpoint( ): auth_header = {"apikey": authentication_secret.value} if authentication_secret.auth_type == "apikey" \ else {"Authorization": authentication_secret.value} - resp = getattr(requests, method)(f"{url}/{endpoints}", headers={ + resp = getattr(requests, method)(f"{url}{endpoints}", headers={ **auth_header, "X-Request-ID": None }) @@ -38,7 +38,7 @@ def test_header_mi_endpoint( ): auth_header = {"apikey": authentication_secret.value} if authentication_secret.auth_type == "apikey" \ else {"Authorization": authentication_secret.value} - resp = getattr(requests, "post")(f"{url}/{MI_ENDPOINT}", headers={ + resp = getattr(requests, "post")(f"{url}{MI_ENDPOINT}", headers={ **auth_header, "X-Request-ID": "" }) diff --git a/tests/e2e-tests/api/letters/test_get_letter_status.py b/tests/e2e-tests/api/letters/test_get_letter_status.py index add29d0b3..9cfa22175 100644 --- a/tests/e2e-tests/api/letters/test_get_letter_status.py +++ b/tests/e2e-tests/api/letters/test_get_letter_status.py @@ -16,8 +16,8 @@ def test_200_get_letter_status(url, authentication_secret): ids = get_pending_letter_ids(url, headers, LETTERS_ENDPOINT, limit=1) letter_id = ids[0] - print(f"calling GET {url}/{LETTERS_ENDPOINT}/{letter_id} with headers {headers}") - get_message_response = requests.get(f"{url}/{LETTERS_ENDPOINT}/{letter_id}", headers=headers) + print(f"calling GET {url}{LETTERS_ENDPOINT}/{letter_id} with headers {headers}") + get_message_response = requests.get(f"{url}{LETTERS_ENDPOINT}/{letter_id}", headers=headers) ErrorHandler.handle_retry(get_message_response) assert get_message_response.status_code == 200, f"Response: {get_message_response.status_code}: {get_message_response.text}" @@ -28,7 +28,9 @@ def test_200_get_letter_status(url, authentication_secret): @pytest.mark.prodtest def test_404_letter_does_not_exist(url, authentication_secret): headers = Generators.generate_valid_headers(authentication_secret) - get_message_response = requests.get(f"{url}/{LETTERS_ENDPOINT}/xx", headers=headers) + + print(f"calling GET {url}{LETTERS_ENDPOINT}/xx with headers {headers}") + get_message_response = requests.get(f"{url}{LETTERS_ENDPOINT}/xx", headers=headers) ErrorHandler.handle_retry(get_message_response) assert get_message_response.status_code == 404, f"Response: {get_message_response.status_code}: {get_message_response.text}" diff --git a/tests/e2e-tests/api/letters/test_get_list_of_letters.py b/tests/e2e-tests/api/letters/test_get_list_of_letters.py index 275d255a6..1c67edb91 100644 --- a/tests/e2e-tests/api/letters/test_get_list_of_letters.py +++ b/tests/e2e-tests/api/letters/test_get_list_of_letters.py @@ -12,5 +12,6 @@ def test_200_get_letters(url, authentication_secret): headers = Generators.generate_valid_headers(authentication_secret) ids = get_pending_letter_ids(url, headers, LETTERS_ENDPOINT, limit=1) + assert ids, "Expected at least one PENDING letter" assert len(ids) == 1 diff --git a/tests/e2e-tests/api/letters/test_multiple_letter_status.py b/tests/e2e-tests/api/letters/test_multiple_letter_status.py index 744fbb8d4..51fe72ddf 100644 --- a/tests/e2e-tests/api/letters/test_multiple_letter_status.py +++ b/tests/e2e-tests/api/letters/test_multiple_letter_status.py @@ -18,8 +18,9 @@ def test_202_with_valid_headers(url, authentication_secret): ids = get_pending_letter_ids(url, headers, LETTERS_ENDPOINT, limit=2) data = Generators.generate_multiple_valid_request(ids) + print(f"calling POST {url}{LETTERS_ENDPOINT} with headers {headers} and body {data}") update_letter_status = requests.post( - f"{url}/{LETTERS_ENDPOINT}", + f"{url}{LETTERS_ENDPOINT}", headers=headers, json=data, ) @@ -37,8 +38,9 @@ def test_400_duplicates_in_request_body(url, authentication_secret): ids = get_pending_letter_ids(url, headers, LETTERS_ENDPOINT, limit=2) data = Generators.generate_duplicate_request(ids) + print(f"calling POST {url}{LETTERS_ENDPOINT} with headers {headers} and body {data}") update_letter_status = requests.post( - f"{url}/{LETTERS_ENDPOINT}", + f"{url}{LETTERS_ENDPOINT}", headers=headers, json=data, ) @@ -57,8 +59,9 @@ def test_400_invalid_status_in_request_body(url, authentication_secret): ids = get_pending_letter_ids(url, headers, LETTERS_ENDPOINT, limit=3) data = Generators.generate_invalid_status_request(ids) + print(f"calling POST {url}{LETTERS_ENDPOINT} with headers {headers} and body {data}") update_letter_status = requests.post( - f"{url}/{LETTERS_ENDPOINT}", + f"{url}{LETTERS_ENDPOINT}", headers=headers, json=data, ) diff --git a/tests/e2e-tests/api/letters/test_update_letter_status.py b/tests/e2e-tests/api/letters/test_update_letter_status.py index 28a61c3ff..98200ec22 100644 --- a/tests/e2e-tests/api/letters/test_update_letter_status.py +++ b/tests/e2e-tests/api/letters/test_update_letter_status.py @@ -19,8 +19,10 @@ def test_202_with_valid_headers(url, authentication_secret): letter_id = ids[0] data = Generators.generate_valid_message_body("ACCEPTED", letter_id) + + print(f"calling PATCH {url}{LETTERS_ENDPOINT}/{letter_id} with headers {headers} and body {data}") update_letter_status = requests.patch( - f"{url}/{LETTERS_ENDPOINT}/{letter_id}", + f"{url}{LETTERS_ENDPOINT}/{letter_id}", headers=headers, json=data, ) @@ -35,8 +37,10 @@ def test_202_with_rejected_status(url, authentication_secret): letter_id = ids[0] data = Generators.generate_valid_message_rejected("REJECTED", letter_id) + + print(f"calling PATCH {url}{LETTERS_ENDPOINT}/{letter_id} with headers {headers} and body {data}") update_letter_status = requests.patch( - f"{url}/{LETTERS_ENDPOINT}/{letter_id}", + f"{url}{LETTERS_ENDPOINT}/{letter_id}", headers=headers, json=data, ) @@ -55,8 +59,10 @@ def test_400_with_invalid_status(url, authentication_secret): letter_id = ids[0] data = Generators.generate_valid_message_body("", letter_id) + + print(f"calling PATCH {url}{LETTERS_ENDPOINT}/{letter_id} with headers {headers} and body {data}") update_letter_status = requests.patch( - f"{url}/{LETTERS_ENDPOINT}/{letter_id}", + f"{url}{LETTERS_ENDPOINT}/{letter_id}", headers=headers, json=data, ) @@ -75,8 +81,10 @@ def test_400_id_mismatch_with_request(url, authentication_secret): letter_id = ids[0] data = Generators.generate_valid_message_body("ACCEPTED", "letter1") + + print(f"calling PATCH {url}{LETTERS_ENDPOINT}/{letter_id} with headers {headers} and body {data}") update_letter_status = requests.patch( - f"{url}/{LETTERS_ENDPOINT}/{letter_id}", + f"{url}{LETTERS_ENDPOINT}/{letter_id}", headers=headers, json=data, ) diff --git a/tests/e2e-tests/lib/letters.py b/tests/e2e-tests/lib/letters.py index 574281fbf..4a1774ecb 100644 --- a/tests/e2e-tests/lib/letters.py +++ b/tests/e2e-tests/lib/letters.py @@ -8,7 +8,7 @@ _REPO_ROOT = pathlib.Path(__file__).resolve().parents[3] _CLI_WORKSPACE = "nhs-notify-supplier-api-letter-test-data-utility" -_SUPPLIER_ID = "TestSupplier1" +_SUPPLIER_ID = "supplier1" # This should be the same id registered in the Apigee App to which the proxy will be associated def create_test_data(count: int = 10) -> None: @@ -68,18 +68,19 @@ def get_pending_letter_ids( deadline = time.monotonic() + timeout_s data = [] while time.monotonic() < deadline: + # Retrieves letters based on the supplier registered in the Apigee App response = requests.get( - f"{url}/{letters_endpoint}?limit={limit}", headers=headers + f"{url}{letters_endpoint}?limit={limit}", headers=headers ) ErrorHandler.handle_retry(response) response.raise_for_status() data.extend(response.json().get("data", [])) if len(data) >= limit: - print(f"Created and found letters with IDs {[item.get('id') for item in data]}") + print(f"Created and found letters with IDs {[item.get('id') for item in data]} for supplier registered in the Apigee App, to which the proxy is associated") return [item.get("id") for item in data] time.sleep(interval_s) raise TimeoutError( f"Timed out after {retries} retries waiting for {limit} PENDING letter(s) at " - f"{url}/{letters_endpoint}" + f"{url}{letters_endpoint}" ) From a213110789d129f5ceb95e44c38c582fc64e5f76 Mon Sep 17 00:00:00 2001 From: Francisco Videira Date: Mon, 11 May 2026 14:28:48 +0000 Subject: [PATCH 06/19] Fix pact tests --- package-lock.json | 366 ++++++++++-------- package.json | 8 +- tests/package.json | 4 +- ...ter-request-prepared.consumer.pact.test.ts | 2 +- ...ter-request-prepared.provider.pact.test.ts | 2 +- 5 files changed, 209 insertions(+), 173 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6d9347d30..f3b0abe38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,12 @@ "tests", "tests/contracts/*" ], + "dependencies": { + "@nhsdigital/nhs-notify-event-schemas-letter-rendering": "^2.0.2", + "@nhsdigital/notify-digital-letters-consumer-contracts": "^1.0.1", + "@pact-foundation/pact": "^16.4.0", + "@pact-foundation/pact-core": "^19.2.0" + }, "devDependencies": { "@aws-sdk/client-api-gateway": "^3.906.0", "@aws-sdk/client-kinesis": "^3.939.0", @@ -259,6 +265,15 @@ "zod": "^4.1.11" } }, + "lambdas/supplier-allocator/node_modules/@nhsdigital/nhs-notify-event-schemas-letter-rendering": { + "version": "2.0.1", + "resolved": "https://npm.pkg.github.com/download/@nhsdigital/nhs-notify-event-schemas-letter-rendering/2.0.1/23a5011fb0addd3da400f798bb1e4340440d62a5", + "integrity": "sha512-U2AWQEBcTDSxA3RX29fdmwjaOPQvwrCQP5rVCgLgtlPGes4Wl695VLw7tDxgpyLY1p9ct3HHrx3Wc6k/QGeW7g==", + "license": "MIT", + "dependencies": { + "zod": "^4.0.17" + } + }, "lambdas/supplier-allocator/node_modules/pino": { "version": "9.14.0", "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", @@ -326,6 +341,15 @@ "zod": "^4.1.11" } }, + "lambdas/upsert-letter/node_modules/@nhsdigital/nhs-notify-event-schemas-letter-rendering": { + "version": "2.0.1", + "resolved": "https://npm.pkg.github.com/download/@nhsdigital/nhs-notify-event-schemas-letter-rendering/2.0.1/23a5011fb0addd3da400f798bb1e4340440d62a5", + "integrity": "sha512-U2AWQEBcTDSxA3RX29fdmwjaOPQvwrCQP5rVCgLgtlPGes4Wl695VLw7tDxgpyLY1p9ct3HHrx3Wc6k/QGeW7g==", + "license": "MIT", + "dependencies": { + "zod": "^4.0.17" + } + }, "node_modules/@apidevtools/json-schema-ref-parser": { "version": "11.9.3", "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz", @@ -5131,9 +5155,9 @@ } }, "node_modules/@nhsdigital/nhs-notify-event-schemas-letter-rendering": { - "version": "2.0.1", - "resolved": "https://npm.pkg.github.com/download/@nhsdigital/nhs-notify-event-schemas-letter-rendering/2.0.1/23a5011fb0addd3da400f798bb1e4340440d62a5", - "integrity": "sha512-U2AWQEBcTDSxA3RX29fdmwjaOPQvwrCQP5rVCgLgtlPGes4Wl695VLw7tDxgpyLY1p9ct3HHrx3Wc6k/QGeW7g==", + "version": "2.0.2", + "resolved": "https://npm.pkg.github.com/download/@nhsdigital/nhs-notify-event-schemas-letter-rendering/2.0.2/a72687375c465d68104b52e58ec67ed59bd99447", + "integrity": "sha512-iSXx98XLFUdz7R4ffy+WoMPi2GEZdJOMA3WHgfR5w+VNPd4aN73oNfNMwqXLViNA6M9HC1JPmudZUueqJQ2KxQ==", "license": "MIT", "dependencies": { "zod": "^4.0.17" @@ -5612,12 +5636,12 @@ } }, "node_modules/@pact-foundation/pact": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/@pact-foundation/pact/-/pact-16.2.0.tgz", - "integrity": "sha512-PFedoP49sR9EKEvhREsXiDTb+rS3yv016DIlufey6pXC6ssQKhgq78ngZDRGl7mc6PqLJdOBCZUDPUrR890k2A==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact/-/pact-16.4.0.tgz", + "integrity": "sha512-nf+xA6cvWlIpwsJ8gDAw69lGRlkqbiU9Ui6ie+ALH8N6+12Ukj7idYod0Zl643FCsActBVWytr5j28dEG0rEQg==", "license": "MIT", "dependencies": { - "@pact-foundation/pact-core": "^18.1.0", + "@pact-foundation/pact-core": "^19.2.0", "axios": "^1.12.2", "body-parser": "^2.2.0", "chalk": "4.1.2", @@ -5638,9 +5662,9 @@ } }, "node_modules/@pact-foundation/pact-core": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core/-/pact-core-17.1.0.tgz", - "integrity": "sha512-0yAUBpLP9ggibw3uX8FW8gHj6zbxCiGNDh1K9oG9b6opzqD3ZsGD8YaKYOHOLTQSoQRsB//Kkeztvi4IfWO3iQ==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core/-/pact-core-19.2.0.tgz", + "integrity": "sha512-7EB2e850hmBzGnwOYTRj4TCFCJQa9zaOy53bK+ybzEDx801olf0gVlYqMYHt1EsHUZzmtS5uDybpr9UMcZQxgg==", "cpu": [ "x64", "ia32", @@ -5658,25 +5682,25 @@ "node-gyp-build": "^4.6.0", "pino": "^10.0.0", "pino-pretty": "^13.1.1", - "underscore": "1.13.7" + "underscore": "1.13.8" }, "engines": { "node": ">=20" }, "optionalDependencies": { - "@pact-foundation/pact-core-darwin-arm64": "17.1.0", - "@pact-foundation/pact-core-darwin-x64": "17.1.0", - "@pact-foundation/pact-core-linux-arm64-glibc": "17.1.0", - "@pact-foundation/pact-core-linux-arm64-musl": "17.1.0", - "@pact-foundation/pact-core-linux-x64-glibc": "17.1.0", - "@pact-foundation/pact-core-linux-x64-musl": "17.1.0", - "@pact-foundation/pact-core-windows-x64": "17.1.0" + "@pact-foundation/pact-core-darwin-arm64": "19.2.0", + "@pact-foundation/pact-core-darwin-x64": "19.2.0", + "@pact-foundation/pact-core-linux-arm64-glibc": "19.2.0", + "@pact-foundation/pact-core-linux-arm64-musl": "19.2.0", + "@pact-foundation/pact-core-linux-x64-glibc": "19.2.0", + "@pact-foundation/pact-core-linux-x64-musl": "19.2.0", + "@pact-foundation/pact-core-windows-x64": "19.2.0" } }, "node_modules/@pact-foundation/pact-core-darwin-arm64": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-darwin-arm64/-/pact-core-darwin-arm64-17.1.0.tgz", - "integrity": "sha512-S4+VgqpuG2/0V7JRdDA9HvdOh38h45mEGr0m5Dqdh23hOvhRQHF25f3ylBpem6of+LacNIqJ+eyBEm4k/0H8MQ==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-darwin-arm64/-/pact-core-darwin-arm64-19.2.0.tgz", + "integrity": "sha512-cQvvWfJKS7iMzkunzh/kdgmyVLhAxyr/BQ8fOnMco2M/1+GHR+sOXvhrR1tes+NAYd1xjE3fbg1K2Flno8aDBw==", "cpu": [ "arm64" ], @@ -5687,9 +5711,9 @@ ] }, "node_modules/@pact-foundation/pact-core-darwin-x64": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-darwin-x64/-/pact-core-darwin-x64-17.1.0.tgz", - "integrity": "sha512-Ex7kykXXq4kyu9NHxvKzwV6yItXZduTF+Ui4dR316xaw7hzTQ+WWnHs0fPFlYFroO/LbWCFBzO5zUUkdU1UknQ==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-darwin-x64/-/pact-core-darwin-x64-19.2.0.tgz", + "integrity": "sha512-UltPXDZ+h1r3JqgIpEBP16bQzB90otd1QbcoeM3XakF9khCrYhW4y9llSuaosyqPFYwCk+mLaZTl/4iitJJaow==", "cpu": [ "x64" ], @@ -5700,9 +5724,9 @@ ] }, "node_modules/@pact-foundation/pact-core-linux-arm64-glibc": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-linux-arm64-glibc/-/pact-core-linux-arm64-glibc-17.1.0.tgz", - "integrity": "sha512-bz34LVZz9DNJWUCwIkq71ZBkSSstjr4febDpnOy/JXPvxuDbVZ6OIy8L8vV24c6JJNTxC1E7z194yxz/zuGAKw==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-linux-arm64-glibc/-/pact-core-linux-arm64-glibc-19.2.0.tgz", + "integrity": "sha512-amiv4COurH1FRa7DSH3ks4UPQCb5J3x4GKrArf8UsTOkNhVGFUWVl83LOELj9Gtk67GwJ96iF7/fQps8I/iFIA==", "cpu": [ "arm64" ], @@ -5713,9 +5737,9 @@ ] }, "node_modules/@pact-foundation/pact-core-linux-arm64-musl": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-linux-arm64-musl/-/pact-core-linux-arm64-musl-17.1.0.tgz", - "integrity": "sha512-NE+1rEMhheBNo8UbUi2bUfjvLwhV9QkY+k/6M+VUvGMVoeNhTXAYBMqz13lWkCcMh4IUbjCaVm6GQrMnV6DV5g==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-linux-arm64-musl/-/pact-core-linux-arm64-musl-19.2.0.tgz", + "integrity": "sha512-tGWF7dP72Ho1A1PApyUqUMRI/e+gfbPxfaXxwWMXNh8JLK2jQQnP4bWymGwHQ4vxL7wn8ayx8I5y6x1vk/2+rA==", "cpu": [ "arm64" ], @@ -5726,9 +5750,9 @@ ] }, "node_modules/@pact-foundation/pact-core-linux-x64-glibc": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-linux-x64-glibc/-/pact-core-linux-x64-glibc-17.1.0.tgz", - "integrity": "sha512-FgTeIVV+/2fCZaKEQN7MCXTNuHzBAT0d8TBGwiQXt6AzxNG9WvqqxpJIzH0mzjVmot3Q+8K4pySiSin/n4Y5CA==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-linux-x64-glibc/-/pact-core-linux-x64-glibc-19.2.0.tgz", + "integrity": "sha512-/ZNuWKuFJSBRZH09t7FXqKLy5koCaMIZ4RdbockLYlNo8uWF2ALfRCllj+SbXPLK+T+lVnBrs5s94X2cu4Rc7A==", "cpu": [ "x64" ], @@ -5739,9 +5763,9 @@ ] }, "node_modules/@pact-foundation/pact-core-linux-x64-musl": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-linux-x64-musl/-/pact-core-linux-x64-musl-17.1.0.tgz", - "integrity": "sha512-2wB65MO1QxH6HvXSVjj8Ii6nRrvEh5Y2O5b7vy7xHAPCbXBR67A/mUw9cxTZLdomAZEaOZN/34p+KXAoTA9JHg==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-linux-x64-musl/-/pact-core-linux-x64-musl-19.2.0.tgz", + "integrity": "sha512-n0m39CzVkq8J2alir1redm0rAdUVQzkKJqYBQdRmhEIa+QeGjUrJq+gmakUysoJxfSY4UthnJ9jS8SoyiZ+k6A==", "cpu": [ "x64" ], @@ -5752,136 +5776,9 @@ ] }, "node_modules/@pact-foundation/pact-core-windows-x64": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-windows-x64/-/pact-core-windows-x64-17.1.0.tgz", - "integrity": "sha512-iKpoKzUkcMUcdc5AbwLJDGNTv64DC0hZEh1xlyysIw6dbQkCcmgAx0Sjw8j7nBH/VQNxrCOaaN54fHzYgVmL9g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@pact-foundation/pact/node_modules/@pact-foundation/pact-core": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core/-/pact-core-18.1.0.tgz", - "integrity": "sha512-QWdntTsTT32r3SOTDaKjB9QyEbbgfsshsY2X/OwBJaNq28jKYEw50t2lYrITN+SdhkgfkEZJ9Y0XNfxtOUDVnA==", - "cpu": [ - "x64", - "ia32", - "arm64" - ], - "license": "MIT", - "os": [ - "darwin", - "linux", - "win32" - ], - "dependencies": { - "check-types": "11.2.3", - "detect-libc": "^2.0.3", - "node-gyp-build": "^4.6.0", - "pino": "^10.0.0", - "pino-pretty": "^13.1.1", - "underscore": "1.13.7" - }, - "engines": { - "node": ">=20" - }, - "optionalDependencies": { - "@pact-foundation/pact-core-darwin-arm64": "18.1.0", - "@pact-foundation/pact-core-darwin-x64": "18.1.0", - "@pact-foundation/pact-core-linux-arm64-glibc": "18.1.0", - "@pact-foundation/pact-core-linux-arm64-musl": "18.1.0", - "@pact-foundation/pact-core-linux-x64-glibc": "18.1.0", - "@pact-foundation/pact-core-linux-x64-musl": "18.1.0", - "@pact-foundation/pact-core-windows-x64": "18.1.0" - } - }, - "node_modules/@pact-foundation/pact/node_modules/@pact-foundation/pact-core-darwin-arm64": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-darwin-arm64/-/pact-core-darwin-arm64-18.1.0.tgz", - "integrity": "sha512-j1GSx7zN011xOveWeBP8RJ77bryFSLHIhe1ZcepfhMiB6VoUjGq1BNQ9rbhO4FSF2HBUCddMAw2/Xa+scHQJvQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@pact-foundation/pact/node_modules/@pact-foundation/pact-core-darwin-x64": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-darwin-x64/-/pact-core-darwin-x64-18.1.0.tgz", - "integrity": "sha512-KV08M7WJm/uuXv2qES4s8oJw8uTgFjQXmHLq4tKGT8HkaYCuSoQAhWYafxBJ/sMHeZ1LoGfv4nJkUr8bNakCYg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@pact-foundation/pact/node_modules/@pact-foundation/pact-core-linux-arm64-glibc": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-linux-arm64-glibc/-/pact-core-linux-arm64-glibc-18.1.0.tgz", - "integrity": "sha512-BeJqmtBR6IdOg2IU0Y+N1NsH1pzr5H3PeOJ+vTSgNiTnV/wYkKk6GZDT1dzwUVi+OjfarGbK86022EoHk/WpkQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@pact-foundation/pact/node_modules/@pact-foundation/pact-core-linux-arm64-musl": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-linux-arm64-musl/-/pact-core-linux-arm64-musl-18.1.0.tgz", - "integrity": "sha512-twakwextRNwkAKntYnSBBAs3yugORGDZwgihVm9p+eYqded/agFTu2v4/E7xksLEotGoFlewnAvLSCTvyNf0uw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@pact-foundation/pact/node_modules/@pact-foundation/pact-core-linux-x64-glibc": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-linux-x64-glibc/-/pact-core-linux-x64-glibc-18.1.0.tgz", - "integrity": "sha512-rhR5iZS77Ie0ocJmtoTub82lyMQmjjn15UPhD5Tv1i2kYkbLCVPSYZpTrKak/OCDm5/AM0Lb7YqIZlOipMm/mA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@pact-foundation/pact/node_modules/@pact-foundation/pact-core-linux-x64-musl": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-linux-x64-musl/-/pact-core-linux-x64-musl-18.1.0.tgz", - "integrity": "sha512-u0N5uU3hwupGCkSmIiehR4yw5Foln+qqkUWXjtKdnttsj8Dz065K8osNGy18jmPRWr5XfRZurnnmw0+tFyaZPA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@pact-foundation/pact/node_modules/@pact-foundation/pact-core-windows-x64": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-windows-x64/-/pact-core-windows-x64-18.1.0.tgz", - "integrity": "sha512-ST7XNlI78c2MCvWvqv7on9M+DmNGlrihiD3Uk5jwZIAWEXeXEVinpfnM/Z5qFik4m3UCDe/Zu/1aNNiY1RN3dQ==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-windows-x64/-/pact-core-windows-x64-19.2.0.tgz", + "integrity": "sha512-WZSDBL1Sn8y5+YJTK+HDiS/Fn9th1M4QJW2dflQ8UFwoaIh6yb0TrJjtuWbDGIwNeD4Sv8CJ5jJOf1nyNjFLfg==", "cpu": [ "x64" ], @@ -23278,9 +23175,9 @@ "@aws-sdk/client-lambda": "^3.986.0", "@aws-sdk/client-sns": "^3.984.0", "@aws-sdk/lib-dynamodb": "^3.1008.0", - "@nhsdigital/nhs-notify-event-schemas-letter-rendering": "^2.0.1", + "@nhsdigital/nhs-notify-event-schemas-letter-rendering": "^2.0.2", "@nhsdigital/notify-digital-letters-consumer-contracts": "^1.0.1", - "@pact-foundation/pact": "^16.0.4", + "@pact-foundation/pact": "^16.4.0", "@pact-foundation/pact-core": "^17.1.0", "@playwright/test": "^1.57.0", "allure-js-commons": "^3.3.3", @@ -23312,6 +23209,133 @@ "@pact-foundation/pact": "^16.0.4" } }, + "tests/node_modules/@pact-foundation/pact-core": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core/-/pact-core-17.1.0.tgz", + "integrity": "sha512-0yAUBpLP9ggibw3uX8FW8gHj6zbxCiGNDh1K9oG9b6opzqD3ZsGD8YaKYOHOLTQSoQRsB//Kkeztvi4IfWO3iQ==", + "cpu": [ + "x64", + "ia32", + "arm64" + ], + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32" + ], + "dependencies": { + "check-types": "11.2.3", + "detect-libc": "^2.0.3", + "node-gyp-build": "^4.6.0", + "pino": "^10.0.0", + "pino-pretty": "^13.1.1", + "underscore": "1.13.7" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "@pact-foundation/pact-core-darwin-arm64": "17.1.0", + "@pact-foundation/pact-core-darwin-x64": "17.1.0", + "@pact-foundation/pact-core-linux-arm64-glibc": "17.1.0", + "@pact-foundation/pact-core-linux-arm64-musl": "17.1.0", + "@pact-foundation/pact-core-linux-x64-glibc": "17.1.0", + "@pact-foundation/pact-core-linux-x64-musl": "17.1.0", + "@pact-foundation/pact-core-windows-x64": "17.1.0" + } + }, + "tests/node_modules/@pact-foundation/pact-core-darwin-arm64": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-darwin-arm64/-/pact-core-darwin-arm64-17.1.0.tgz", + "integrity": "sha512-S4+VgqpuG2/0V7JRdDA9HvdOh38h45mEGr0m5Dqdh23hOvhRQHF25f3ylBpem6of+LacNIqJ+eyBEm4k/0H8MQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "tests/node_modules/@pact-foundation/pact-core-darwin-x64": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-darwin-x64/-/pact-core-darwin-x64-17.1.0.tgz", + "integrity": "sha512-Ex7kykXXq4kyu9NHxvKzwV6yItXZduTF+Ui4dR316xaw7hzTQ+WWnHs0fPFlYFroO/LbWCFBzO5zUUkdU1UknQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "tests/node_modules/@pact-foundation/pact-core-linux-arm64-glibc": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-linux-arm64-glibc/-/pact-core-linux-arm64-glibc-17.1.0.tgz", + "integrity": "sha512-bz34LVZz9DNJWUCwIkq71ZBkSSstjr4febDpnOy/JXPvxuDbVZ6OIy8L8vV24c6JJNTxC1E7z194yxz/zuGAKw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "tests/node_modules/@pact-foundation/pact-core-linux-arm64-musl": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-linux-arm64-musl/-/pact-core-linux-arm64-musl-17.1.0.tgz", + "integrity": "sha512-NE+1rEMhheBNo8UbUi2bUfjvLwhV9QkY+k/6M+VUvGMVoeNhTXAYBMqz13lWkCcMh4IUbjCaVm6GQrMnV6DV5g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "tests/node_modules/@pact-foundation/pact-core-linux-x64-glibc": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-linux-x64-glibc/-/pact-core-linux-x64-glibc-17.1.0.tgz", + "integrity": "sha512-FgTeIVV+/2fCZaKEQN7MCXTNuHzBAT0d8TBGwiQXt6AzxNG9WvqqxpJIzH0mzjVmot3Q+8K4pySiSin/n4Y5CA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "tests/node_modules/@pact-foundation/pact-core-linux-x64-musl": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-linux-x64-musl/-/pact-core-linux-x64-musl-17.1.0.tgz", + "integrity": "sha512-2wB65MO1QxH6HvXSVjj8Ii6nRrvEh5Y2O5b7vy7xHAPCbXBR67A/mUw9cxTZLdomAZEaOZN/34p+KXAoTA9JHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "tests/node_modules/@pact-foundation/pact-core-windows-x64": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-windows-x64/-/pact-core-windows-x64-17.1.0.tgz", + "integrity": "sha512-iKpoKzUkcMUcdc5AbwLJDGNTv64DC0hZEh1xlyysIw6dbQkCcmgAx0Sjw8j7nBH/VQNxrCOaaN54fHzYgVmL9g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "tests/node_modules/dotenv": { "version": "17.3.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", @@ -23323,6 +23347,12 @@ "funding": { "url": "https://dotenvx.com" } + }, + "tests/node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "license": "MIT" } } } diff --git a/package.json b/package.json index 747493a93..6f1faf765 100644 --- a/package.json +++ b/package.json @@ -135,5 +135,11 @@ "scripts/utilities/*", "tests", "tests/contracts/*" - ] + ], + "dependencies": { + "@nhsdigital/nhs-notify-event-schemas-letter-rendering": "^2.0.2", + "@nhsdigital/notify-digital-letters-consumer-contracts": "^1.0.1", + "@pact-foundation/pact": "^16.4.0", + "@pact-foundation/pact-core": "^19.2.0" + } } diff --git a/tests/package.json b/tests/package.json index c096908da..fba34ec03 100644 --- a/tests/package.json +++ b/tests/package.json @@ -7,9 +7,9 @@ "@aws-sdk/client-lambda": "^3.986.0", "@aws-sdk/client-sns": "^3.984.0", "@aws-sdk/lib-dynamodb": "^3.1008.0", - "@nhsdigital/nhs-notify-event-schemas-letter-rendering": "^2.0.1", + "@nhsdigital/nhs-notify-event-schemas-letter-rendering": "^2.0.2", "@nhsdigital/notify-digital-letters-consumer-contracts": "^1.0.1", - "@pact-foundation/pact": "^16.0.4", + "@pact-foundation/pact": "^16.4.0", "@pact-foundation/pact-core": "^17.1.0", "@playwright/test": "^1.57.0", "allure-js-commons": "^3.3.3", diff --git a/tests/pact-tests/consumer/letter-request-prepared.consumer.pact.test.ts b/tests/pact-tests/consumer/letter-request-prepared.consumer.pact.test.ts index b73de11f3..c4c2a51cf 100644 --- a/tests/pact-tests/consumer/letter-request-prepared.consumer.pact.test.ts +++ b/tests/pact-tests/consumer/letter-request-prepared.consumer.pact.test.ts @@ -37,7 +37,7 @@ describe("Pact Message Consumer - LetterRequestPrepared Event", () => { datacontenttype: "application/json", dataschema: MatchersV3.regex( /^https:\/\/notify\.nhs\.uk\/cloudevents\/schemas\/letter-rendering\/letter-request\.prepared\.2\.\d+\.\d+\.schema\.json$/, - "https://notify.nhs.uk/cloudevents/schemas/letter-rendering/letter-request.prepared.2.0.1.schema.json", + "https://notify.nhs.uk/cloudevents/schemas/letter-rendering/letter-request.prepared.2.0.2.schema.json", ), dataschemaversion: MatchersV3.regex(/\d+\.\d+\.\d+/, "2.0.0"), traceparent: MatchersV3.string( diff --git a/tests/pact-tests/provider/letter-request-prepared.provider.pact.test.ts b/tests/pact-tests/provider/letter-request-prepared.provider.pact.test.ts index 9fbf33842..d9c2d4b65 100644 --- a/tests/pact-tests/provider/letter-request-prepared.provider.pact.test.ts +++ b/tests/pact-tests/provider/letter-request-prepared.provider.pact.test.ts @@ -1,6 +1,6 @@ import path from "node:path"; import { MessageProviderPact } from "@pact-foundation/pact"; -import LetterRequestPreparedEvent from "@nhsdigital/nhs-notify-event-schemas-letter-rendering/examples/LetterRequestPrepared/v2.0.1.json"; +import LetterRequestPreparedEvent from "@nhsdigital/nhs-notify-event-schemas-letter-rendering/examples/LetterRequestPrepared/v2.0.2.json"; describe("Letter rendering message provider tests", () => { test("verify pacts", async () => { From 64f50150b6cfcbc1222754a99618ceea8a17a6c1 Mon Sep 17 00:00:00 2001 From: Francisco Videira Date: Tue, 12 May 2026 09:51:39 +0000 Subject: [PATCH 07/19] create-letter-batch logs letterIds, e2e read and wait for queue --- .env.template | 5 ++- .../letter-test-data/src/cli/index.ts | 3 ++ tests/e2e-tests/lib/letters.py | 36 +++++++++++++++---- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/.env.template b/.env.template index 69e2590a1..55e341c3d 100644 --- a/.env.template +++ b/.env.template @@ -1,7 +1,10 @@ ENVIRONMENT=$ENV_NAME API_KEY= HEADERAUTH= -PR_NUMBER=prxx # remove if needs to run against main +# remove if needs to run against main +PR_NUMBER=prxx +# remove if needs to run against main +TARGET_ENVIRONMENT=prxx NHSD_APIM_TOKEN= PROXY_NAME= # * nhs-notify-supplier--internal-dev--nhs-notify-supplier diff --git a/scripts/utilities/letter-test-data/src/cli/index.ts b/scripts/utilities/letter-test-data/src/cli/index.ts index a8ddc8ffb..0b6c32371 100644 --- a/scripts/utilities/letter-test-data/src/cli/index.ts +++ b/scripts/utilities/letter-test-data/src/cli/index.ts @@ -229,6 +229,9 @@ async function main() { await letterRepository.unsafePutLetterBatch(letterDtos); console.log(`Created batch ${batchId} of ${letterDtos.length} letters`); + console.log( + `LETTER_IDS:${JSON.stringify(letterDtos.map(({ id }) => id))}`, + ); }, ) .demandCommand(1) diff --git a/tests/e2e-tests/lib/letters.py b/tests/e2e-tests/lib/letters.py index 4a1774ecb..ecadca468 100644 --- a/tests/e2e-tests/lib/letters.py +++ b/tests/e2e-tests/lib/letters.py @@ -2,6 +2,7 @@ import subprocess import pathlib import time +import json import requests from lib.errorhandler import ErrorHandler @@ -11,11 +12,13 @@ _SUPPLIER_ID = "supplier1" # This should be the same id registered in the Apigee App to which the proxy will be associated -def create_test_data(count: int = 10) -> None: +def create_test_data(count: int = 10) -> list[str]: """Seed PENDING letters by delegating to the shared letter-test-data CLI. Mirrors createTestData() in tests/helpers/generate-fetch-test-data.ts so both test suites seed data through the same tool. + + Returns a list of letter IDs created by the CLI. """ environment = os.environ.get("TARGET_ENVIRONMENT", "main") aws_account_id = os.environ.get("AWS_ACCOUNT_ID", "820178564574") @@ -38,6 +41,7 @@ def create_test_data(count: int = 10) -> None: "--test-letter", "test-letter-standard", ] + print(f"Creating test data by running CLI command: {' '.join(cmd)}") result = subprocess.run(cmd, cwd=_REPO_ROOT, capture_output=True, text=True) if result.returncode != 0: raise RuntimeError( @@ -46,6 +50,22 @@ def create_test_data(count: int = 10) -> None: f"stderr:\n{result.stderr}" ) + ids_prefix = "LETTER_IDS:" + for line in result.stdout.splitlines(): + if line.startswith(ids_prefix): + payload = line[len(ids_prefix):] + data = json.loads(payload) + if isinstance(data, list): + res = [str(item) for item in data] + print(f"The following letter IDs were created: {res}") + return res + + raise RuntimeError( + "create-letter-batch CLI completed successfully but did not output LETTER_IDS.\n" + f"stdout:\n{result.stdout}\n" + f"stderr:\n{result.stderr}" + ) + def get_pending_letter_ids( url: str, @@ -60,24 +80,26 @@ def get_pending_letter_ids( visible via the letters endpoint. Retries to account for other tests running in parallel stealing the letters Returns a list of letter ID strings. - Raises TimeoutError if fewer than `limit` letters are returned after all retries are exhausted. + Raises TimeoutError if the expected number of pending letters do not appear within the timeout period. """ for _ in range(retries): - create_test_data(limit) + letterIds = create_test_data(limit) deadline = time.monotonic() + timeout_s data = [] while time.monotonic() < deadline: # Retrieves letters based on the supplier registered in the Apigee App response = requests.get( - f"{url}{letters_endpoint}?limit={limit}", headers=headers + f"{url}{letters_endpoint}", headers=headers ) ErrorHandler.handle_retry(response) response.raise_for_status() data.extend(response.json().get("data", [])) - if len(data) >= limit: - print(f"Created and found letters with IDs {[item.get('id') for item in data]} for supplier registered in the Apigee App, to which the proxy is associated") - return [item.get("id") for item in data] + idsFound = [ item.get("id") for item in data ] + if set(letterIds).issubset(idsFound): + print(f"Found all created letter IDs: {letterIds}") + return letterIds + print(f"Expected letter IDs {letterIds} not found in response. Retrying in {interval_s} seconds...") time.sleep(interval_s) raise TimeoutError( From ef8440fd2955444e7236bcee2fd4ba1ea9ab3490 Mon Sep 17 00:00:00 2001 From: Francisco Videira Date: Wed, 13 May 2026 17:14:13 +0000 Subject: [PATCH 08/19] env template cleanup --- .env.template | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.env.template b/.env.template index 87eca9f1b..9bd5f13fa 100644 --- a/.env.template +++ b/.env.template @@ -1,19 +1,18 @@ -# remove if needs to run against main -PR_NUMBER=prxx # Your github Personal Access Token (PAT) GITHUB_TOKEN= -# The variables below are used for End to End tests -# information about the proxy name can be found in the tests/e2e-tests/README.md -# e.g. # nhs-notify-supplier--internal-dev--nhs-notify-supplier # nhs-notify-supplier--internal-dev--nhs-notify-supplier-PR-XX # nhs-notify-supplier--ref--nhs-notify-supplier PROXY_NAME= + +#A PIM env to run tests against, other options are: ref, int, prod API_ENVIRONMENT=internal-dev -# 820178564574 Supplier Dev + +# 820178564574 Supplier Dev is the default if removed # See AWS access portal for others AWS_ACCOUNT_ID=820178564574 + # remove if needs to run against main TARGET_ENVIRONMENT=prxx @@ -37,3 +36,7 @@ export NON_PROD_PRIVATE_KEY=xxx export INTEGRATION_PRIVATE_KEY=xxx # private key path used to generate authentication for tests ran against the prod environment export PRODUCTION_PRIVATE_KEY=xxx + +# E2E Test Variables +# ======== +# To set variables for running E2E tests locally see tests/e2e-tests/README.md From cd0fcac6d891380717add5f116d42c155a5613ef Mon Sep 17 00:00:00 2001 From: Francisco Videira Date: Wed, 13 May 2026 17:36:58 +0000 Subject: [PATCH 09/19] env template cleanup 2 --- .env.template | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.env.template b/.env.template index 9bd5f13fa..6aed7e69a 100644 --- a/.env.template +++ b/.env.template @@ -1,18 +1,18 @@ # Your github Personal Access Token (PAT) GITHUB_TOKEN= -# nhs-notify-supplier--internal-dev--nhs-notify-supplier +# Apigee proxy name to be used for test execution # nhs-notify-supplier--internal-dev--nhs-notify-supplier-PR-XX -# nhs-notify-supplier--ref--nhs-notify-supplier PROXY_NAME= -#A PIM env to run tests against, other options are: ref, int, prod +# APIM env to run tests against, other options are: ref, int, prod API_ENVIRONMENT=internal-dev # 820178564574 Supplier Dev is the default if removed # See AWS access portal for others AWS_ACCOUNT_ID=820178564574 +# Resource namespace used to resolve AWS resource names for tests (main, pr123) # remove if needs to run against main TARGET_ENVIRONMENT=prxx From ad3999496a38e464a1fe055fcf9eed2095577255 Mon Sep 17 00:00:00 2001 From: Francisco Videira Date: Thu, 14 May 2026 15:43:54 +0000 Subject: [PATCH 10/19] smoking out failed tests --- Makefile | 2 +- package-lock.json | 9 --------- .../integration-tests/urgent-letter-priority.spec.ts | 3 +++ 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index a0368e2f0..394ec76e1 100644 --- a/Makefile +++ b/Makefile @@ -132,7 +132,7 @@ ${VERBOSE}.SILENT: \ ##################### # https://pytest-xdist.readthedocs.io/en/stable/known-limitations.html#output-stdout-and-stderr-from-workers means pytest won't print to stdout even with -s -PYTEST_WORKERS := 4 # set to 0 to see stdout/stderr when debugging e2e tests +PYTEST_WORKERS := 0 # set to 0 to see stdout/stderr when debugging e2e tests TEST_CMD := APIGEE_ACCESS_TOKEN="$(APIGEE_ACCESS_TOKEN)" \ STATUS_ENDPOINT_API_KEY="$(STATUS_ENDPOINT_API_KEY)" \ diff --git a/package-lock.json b/package-lock.json index c8fcc257a..5d93c4242 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5197,15 +5197,6 @@ "resolved": "internal/events", "link": true }, - "node_modules/@nhsdigital/nhs-notify-event-schemas-supplier-config": { - "version": "1.0.1", - "resolved": "https://npm.pkg.github.com/download/@nhsdigital/nhs-notify-event-schemas-supplier-config/1.0.1/ff1ce566201ae291825acd5e771537229d6aa9ca", - "integrity": "sha512-gIZgfzgvkCfZE+HCosrVJ3tBce2FJRGfwPmtYtZDBG+ox/KvbpJFWXzJ5Jkh/42YzcVn2GxT1fy1L1F6pxiYWA==", - "dependencies": { - "@asyncapi/bundler": "^0.6.4", - "zod": "^4.1.12" - } - }, "node_modules/@nhsdigital/notify-digital-letters-consumer-contracts": { "version": "1.0.1", "resolved": "https://npm.pkg.github.com/download/@nhsdigital/notify-digital-letters-consumer-contracts/1.0.1/a721d9c8b1e01a61de4ecc2b62d3c692e5213bb8", diff --git a/tests/component-tests/integration-tests/urgent-letter-priority.spec.ts b/tests/component-tests/integration-tests/urgent-letter-priority.spec.ts index f4cb2c65c..c49a32d21 100644 --- a/tests/component-tests/integration-tests/urgent-letter-priority.spec.ts +++ b/tests/component-tests/integration-tests/urgent-letter-priority.spec.ts @@ -11,6 +11,7 @@ import { import { createValidRequestHeaders } from "tests/constants/request-headers"; import { SUPPLIER_LETTERS } from "tests/constants/api-constants"; import { supplierDataSetup } from "tests/helpers/suppliers-setup-helper"; +import { logger } from "tests/helpers/pino-logger"; import { GetLettersResponse, GetLettersResponseSchema, @@ -34,6 +35,8 @@ test.describe("Urgent Letter Priority Tests", () => { const urgencyNineLetterIds = await sendEventsForVariants(variantsUrgencyNine); + logger.info({ urgencyNineLetterIds, urgencyTenLetterIds }); + await Promise.all( [...urgencyNineLetterIds, ...urgencyTenLetterIds].map(async (domainId) => pollForLetterStatus(request, supplier, domainId, baseUrl), From 8efb6ede16d7a42c8e8ffa05c4dd1b2582ea63d6 Mon Sep 17 00:00:00 2001 From: Francisco Videira Date: Fri, 15 May 2026 13:12:29 +0000 Subject: [PATCH 11/19] add markers --- .github/workflows/deploy-dynamic-env-proxy.yaml | 4 ++-- tests/e2e-tests/api/letters/test_update_letter_status.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-dynamic-env-proxy.yaml b/.github/workflows/deploy-dynamic-env-proxy.yaml index 7a4313ad1..a61b3035d 100644 --- a/.github/workflows/deploy-dynamic-env-proxy.yaml +++ b/.github/workflows/deploy-dynamic-env-proxy.yaml @@ -1,4 +1,3 @@ -# Test name: Deploy dynamic PR environment proxy run-name: Deploy proxy for PR environment on internal-dev by @${{ github.actor }} @@ -30,7 +29,8 @@ jobs: - name: Resolve nodejs version id: toolversions - run: echo "nodejs_version=$(grep '^nodejs\s' .tool-versions | cut -f2 -d' ')" >> + run: + echo "nodejs_version=$(grep '^nodejs\s' .tool-versions | cut -f2 -d' ')" >> "$GITHUB_OUTPUT" - name: "Check if pull request exists for this branch and set diff --git a/tests/e2e-tests/api/letters/test_update_letter_status.py b/tests/e2e-tests/api/letters/test_update_letter_status.py index 98200ec22..291c84848 100644 --- a/tests/e2e-tests/api/letters/test_update_letter_status.py +++ b/tests/e2e-tests/api/letters/test_update_letter_status.py @@ -30,6 +30,10 @@ def test_202_with_valid_headers(url, authentication_secret): ErrorHandler.handle_retry(update_letter_status) assert update_letter_status.status_code == 202, f"Response: {update_letter_status.status_code}: {update_letter_status.text}" +@pytest.mark.test +@pytest.mark.devtest +@pytest.mark.inttest +@pytest.mark.prodtest def test_202_with_rejected_status(url, authentication_secret): headers = Generators.generate_valid_headers(authentication_secret) From 6a8baeaf4e9d1e60b364a9939215145fe5d29772 Mon Sep 17 00:00:00 2001 From: Francisco Videira Date: Wed, 20 May 2026 14:34:05 +0000 Subject: [PATCH 12/19] npm install --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index b997674e0..78538f2be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -116,7 +116,7 @@ }, "internal/events": { "name": "@nhsdigital/nhs-notify-event-schemas-supplier-api", - "version": "1.0.18", + "version": "1.0.19", "license": "MIT", "dependencies": { "@asyncapi/bundler": "^0.6.4", From fc785c85b81bc3dc3f0a3a8e3388cbbbcd839181 Mon Sep 17 00:00:00 2001 From: Francisco Videira Date: Wed, 20 May 2026 14:46:54 +0000 Subject: [PATCH 13/19] All tests run --- .../action.yml | 0 .github/actions/acceptance-tests/action.yml | 6 +++--- Makefile | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename .github/actions/{acceptance-tests-component => acceptance-tests-components}/action.yml (100%) diff --git a/.github/actions/acceptance-tests-component/action.yml b/.github/actions/acceptance-tests-components/action.yml similarity index 100% rename from .github/actions/acceptance-tests-component/action.yml rename to .github/actions/acceptance-tests-components/action.yml diff --git a/.github/actions/acceptance-tests/action.yml b/.github/actions/acceptance-tests/action.yml index 0ac177dab..ba1048790 100644 --- a/.github/actions/acceptance-tests/action.yml +++ b/.github/actions/acceptance-tests/action.yml @@ -24,9 +24,9 @@ runs: steps: - - name: Run component tests - if: ${{ inputs.testType == 'component' }} - uses: ./.github/actions/acceptance-tests-component + - name: Run components tests (sandbox and component tests) + if: ${{ inputs.testType != 'e2e' }} + uses: ./.github/actions/acceptance-tests-components with: testType: ${{ inputs.testType }} targetEnvironment: ${{ inputs.targetEnvironment }} diff --git a/Makefile b/Makefile index 394ec76e1..a0368e2f0 100644 --- a/Makefile +++ b/Makefile @@ -132,7 +132,7 @@ ${VERBOSE}.SILENT: \ ##################### # https://pytest-xdist.readthedocs.io/en/stable/known-limitations.html#output-stdout-and-stderr-from-workers means pytest won't print to stdout even with -s -PYTEST_WORKERS := 0 # set to 0 to see stdout/stderr when debugging e2e tests +PYTEST_WORKERS := 4 # set to 0 to see stdout/stderr when debugging e2e tests TEST_CMD := APIGEE_ACCESS_TOKEN="$(APIGEE_ACCESS_TOKEN)" \ STATUS_ENDPOINT_API_KEY="$(STATUS_ENDPOINT_API_KEY)" \ From 8dd16ff0b69188fba8715dd1c7e5ce8e6097fb1f Mon Sep 17 00:00:00 2001 From: Francisco Videira Date: Thu, 21 May 2026 18:25:52 +0000 Subject: [PATCH 14/19] ci(tests): pass targetAccountGroup through acceptance composite actions --- .github/actions/acceptance-tests-components/action.yml | 5 +++++ .github/actions/acceptance-tests-e2e/action.yml | 4 ++++ .github/actions/acceptance-tests/action.yml | 5 +++-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/actions/acceptance-tests-components/action.yml b/.github/actions/acceptance-tests-components/action.yml index 810c3b3a3..833966699 100644 --- a/.github/actions/acceptance-tests-components/action.yml +++ b/.github/actions/acceptance-tests-components/action.yml @@ -14,6 +14,10 @@ inputs: description: Name of the component under test required: true + targetAccountGroup: + description: Name of the account group under test (e.g. nhs-notify-supplier-api-dev) + required: true + runs: using: "composite" @@ -39,5 +43,6 @@ runs: shell: bash env: TARGET_ENVIRONMENT: ${{ inputs.targetEnvironment }} + TARGET_ACCOUNT_GROUP: ${{ inputs.targetAccountGroup }} run: | make test-${{ inputs.testType }} diff --git a/.github/actions/acceptance-tests-e2e/action.yml b/.github/actions/acceptance-tests-e2e/action.yml index f536842d4..ae5e2e95c 100644 --- a/.github/actions/acceptance-tests-e2e/action.yml +++ b/.github/actions/acceptance-tests-e2e/action.yml @@ -5,6 +5,9 @@ inputs: targetEnvironment: description: Name of the environment under test required: true + targetAccountGroup: + description: Name of the account group under test + required: true runs: using: "composite" @@ -65,6 +68,7 @@ runs: env: TARGET_ENVIRONMENT: ${{ inputs.targetEnvironment }} PR_NUMBER: ${{ steps.set_pr_number.outputs.pr_number }} + TARGET_ACCOUNT_GROUP: ${{ inputs.targetAccountGroup }} run: | echo "$DEV_E2E_KEYS_PRIVATE" > "${GITHUB_WORKSPACE}/internal-dev-test-1.pem" chmod 600 "${GITHUB_WORKSPACE}/internal-dev-test-1.pem" diff --git a/.github/actions/acceptance-tests/action.yml b/.github/actions/acceptance-tests/action.yml index ba1048790..23370bcff 100644 --- a/.github/actions/acceptance-tests/action.yml +++ b/.github/actions/acceptance-tests/action.yml @@ -11,8 +11,7 @@ inputs: required: true targetAccountGroup: - description: Name of the account group under test - default: nhs-notify-template-management-dev + description: Name of the account group under test (e.g. nhs-notify-supplier-api-dev) required: true targetComponent: @@ -31,9 +30,11 @@ runs: testType: ${{ inputs.testType }} targetEnvironment: ${{ inputs.targetEnvironment }} targetComponent: ${{ inputs.targetComponent }} + targetAccountGroup: ${{ inputs.targetAccountGroup }} - name: Run e2e tests if: ${{ inputs.testType == 'e2e' }} uses: ./.github/actions/acceptance-tests-e2e with: targetEnvironment: ${{ inputs.targetEnvironment }} + targetAccountGroup: ${{ inputs.targetAccountGroup }} From 0a26c0e03cd4482a5a1eb509175988a100ae5735 Mon Sep 17 00:00:00 2001 From: Francisco Videira Date: Thu, 21 May 2026 18:27:41 +0000 Subject: [PATCH 15/19] infra: set shorter letter queue visibility timeout for dev group --- .../terraform/components/api/README.md | 2 +- .../terraform/components/api/locals.tf | 34 +++++++++++-------- .../terraform/components/api/variables.tf | 2 +- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/infrastructure/terraform/components/api/README.md b/infrastructure/terraform/components/api/README.md index 23356b0a4..493cbad2a 100644 --- a/infrastructure/terraform/components/api/README.md +++ b/infrastructure/terraform/components/api/README.md @@ -33,7 +33,7 @@ No requirements. | [eventpub\_data\_plane\_bus\_arn](#input\_eventpub\_data\_plane\_bus\_arn) | ARN of the EventBridge data plane bus for eventpub | `string` | `""` | no | | [force\_destroy](#input\_force\_destroy) | Flag to force deletion of S3 buckets | `bool` | `false` | no | | [force\_lambda\_code\_deploy](#input\_force\_lambda\_code\_deploy) | If the lambda package in s3 has the same commit id tag as the terraform build branch, the lambda will not update automatically. Set to True if making changes to Lambda code from on the same commit for example during development | `bool` | `false` | no | -| [group](#input\_group) | The group variables are being inherited from (often synonmous with account short-name) | `string` | n/a | yes | +| [group](#input\_group) | The account group short-name | `string` | n/a | yes | | [kms\_deletion\_window](#input\_kms\_deletion\_window) | When a kms key is deleted, how long should it wait in the pending deletion state? | `string` | `"30"` | no | | [letter\_event\_source](#input\_letter\_event\_source) | Source value to use for the letter status event updates | `string` | `"/data-plane/supplier-api/nhs-supplier-api-prod/main/update-status"` | no | | [letter\_table\_ttl\_hours](#input\_letter\_table\_ttl\_hours) | Number of hours to set as TTL on letters table | `number` | `24` | no | diff --git a/infrastructure/terraform/components/api/locals.tf b/infrastructure/terraform/components/api/locals.tf index 6975e8d4e..9984e0aad 100644 --- a/infrastructure/terraform/components/api/locals.tf +++ b/infrastructure/terraform/components/api/locals.tf @@ -22,20 +22,26 @@ locals { destination_arn = "arn:aws:logs:${var.region}:${var.shared_infra_account_id}:destination:nhs-main-obs-firehose-logs" common_lambda_env_vars = { - APIM_CORRELATION_HEADER = "nhsd-correlation-id", - DOWNLOAD_URL_TTL_SECONDS = 60 - EVENT_SOURCE = "/data-plane/supplier-api/${var.group}/${var.environment}/letters" - LETTER_TTL_HOURS = 12960, # 18 months * 30 days * 24 hours - LETTER_QUEUE_TABLE_NAME = aws_dynamodb_table.letter_queue.name, - LETTER_QUEUE_TTL_HOURS = 168 # 7 days * 24 hours - LETTER_QUEUE_VISIBILITY_TIMEOUT = 300, # 5 minutes * 60 seconds - LETTERS_TABLE_NAME = aws_dynamodb_table.letters.name, - MI_TABLE_NAME = aws_dynamodb_table.mi.name, - MI_TTL_HOURS = 2160 # 90 days * 24 hours - SNS_TOPIC_ARN = "${module.eventsub.sns_topic.arn}", - SUPPLIER_CONFIG_TABLE_NAME = aws_dynamodb_table.supplier-configuration.name, - SUPPLIER_QUOTAS_TABLE_NAME = aws_dynamodb_table.supplier-quotas.name, - SUPPLIER_ID_HEADER = "nhsd-supplier-id", + APIM_CORRELATION_HEADER = "nhsd-correlation-id", + DOWNLOAD_URL_TTL_SECONDS = 60, + EVENT_SOURCE = "/data-plane/supplier-api/${var.group}/${var.environment}/letters", + LETTER_TTL_HOURS = 12960, # 18 months * 30 days * 24 hours + LETTER_QUEUE_TABLE_NAME = aws_dynamodb_table.letter_queue.name, + LETTER_QUEUE_TTL_HOURS = 168, # 7 days * 24 hours + LETTER_QUEUE_VISIBILITY_TIMEOUT = lookup( + { + nhs-notify-supplier-api-dev = 10, # 10 seconds for development/testing + }, + var.group, + 300, # Default to 5 minutes for other groups (nonprod and prod) + ), + LETTERS_TABLE_NAME = aws_dynamodb_table.letters.name, + MI_TABLE_NAME = aws_dynamodb_table.mi.name, + MI_TTL_HOURS = 2160, # 90 days * 24 hours + SNS_TOPIC_ARN = "${module.eventsub.sns_topic.arn}", + SUPPLIER_CONFIG_TABLE_NAME = aws_dynamodb_table.supplier-configuration.name, + SUPPLIER_QUOTAS_TABLE_NAME = aws_dynamodb_table.supplier-quotas.name, + SUPPLIER_ID_HEADER = "nhsd-supplier-id", } core_pdf_bucket_arn = "arn:aws:s3:::comms-${var.core_account_id}-eu-west-2-${var.core_environment}-api-stg-pdf-pipeline" diff --git a/infrastructure/terraform/components/api/variables.tf b/infrastructure/terraform/components/api/variables.tf index 0c64ff283..bac5b8361 100644 --- a/infrastructure/terraform/components/api/variables.tf +++ b/infrastructure/terraform/components/api/variables.tf @@ -24,7 +24,7 @@ variable "region" { variable "group" { type = string - description = "The group variables are being inherited from (often synonmous with account short-name)" + description = "The account group short-name" } ## From 590215226acc4524f690985654118b5c1b7e47a7 Mon Sep 17 00:00:00 2001 From: Francisco Videira Date: Thu, 21 May 2026 18:28:16 +0000 Subject: [PATCH 16/19] component tests: centralize GET /letters retry handling and update specs --- .../src/services/letter-operations.ts | 1 - .../apiGateway-tests/get-letters.spec.ts | 81 +++++++----- .../urgent-letter-priority.spec.ts | 29 ++--- .../queue-operations.spec.ts | 45 ++++--- tests/helpers/generate-fetch-test-data.ts | 117 +++++++++++++++++- 5 files changed, 206 insertions(+), 67 deletions(-) diff --git a/lambdas/api-handler/src/services/letter-operations.ts b/lambdas/api-handler/src/services/letter-operations.ts index b96b3f8db..a29017f65 100644 --- a/lambdas/api-handler/src/services/letter-operations.ts +++ b/lambdas/api-handler/src/services/letter-operations.ts @@ -41,7 +41,6 @@ function mapPendingLetterToLetterBase(pending: PendingLetterBase): LetterBase { export const getPendingLetters = async ( supplierId: string, - limit: number, letterQueueRepo: LetterQueueRepository, visibilityTimeout: number, diff --git a/tests/component-tests/apiGateway-tests/get-letters.spec.ts b/tests/component-tests/apiGateway-tests/get-letters.spec.ts index d01a97d43..b48544916 100644 --- a/tests/component-tests/apiGateway-tests/get-letters.spec.ts +++ b/tests/component-tests/apiGateway-tests/get-letters.spec.ts @@ -1,11 +1,15 @@ import { expect, test } from "@playwright/test"; -import { SUPPLIER_LETTERS } from "../../constants/api-constants"; import { createHeaderWithNoCorrelationId, createInvalidRequestHeaders, createValidRequestHeaders, } from "../../constants/request-headers"; import getRestApiGatewayBaseUrl from "../../helpers/aws-gateway-helper"; +import { + getLettersWithRetry, + isErrorResponse, + isGetLettersResponse, +} from "../../helpers/generate-fetch-test-data"; let baseUrl: string; @@ -15,31 +19,36 @@ test.beforeAll(async () => { test.describe("API Gateway Tests To Get List Of Pending Letters", () => { test("GET /letters should return 200 and list items", async ({ request }) => { - const header = createValidRequestHeaders(); - const response = await request.get(`${baseUrl}/${SUPPLIER_LETTERS}`, { - headers: header, - params: { - limit: "2", + const headers = createValidRequestHeaders(); + const { responseBody, statusCode } = await getLettersWithRetry( + request, + baseUrl, + headers, + { + lettersLimit: "2", }, - }); + ); - expect(response.status()).toBe(200); - const responseBody = await response.json(); + expect(statusCode).toBe(200); + if (!isGetLettersResponse(responseBody)) { + throw new Error("Expected GetLettersResponse body for 200 status"); + } expect(responseBody.data.length).toBeGreaterThanOrEqual(1); }); test("GET /letters with invalid authentication should return 403", async ({ request, }) => { - const header = createInvalidRequestHeaders(); - const response = await request.get(`${baseUrl}/${SUPPLIER_LETTERS}`, { - headers: header, - params: { - limit: "2", + const headers = createInvalidRequestHeaders(); + const { responseBody, statusCode } = await getLettersWithRetry( + request, + baseUrl, + headers, + { + waitForVisibilityTimeout: false, }, - }); - expect(response.status()).toBe(403); - const responseBody = await response.json(); + ); + expect(statusCode).toBe(403); expect(responseBody).toMatchObject({ Message: "User is not authorized to access this resource with an explicit deny in an identity-based policy", @@ -49,15 +58,19 @@ test.describe("API Gateway Tests To Get List Of Pending Letters", () => { test("GET /letters with empty correlationId should return 500", async ({ request, }) => { - const header = createHeaderWithNoCorrelationId(); - const response = await request.get(`${baseUrl}/${SUPPLIER_LETTERS}`, { - headers: header, - params: { - limit: "2", + const headers = createHeaderWithNoCorrelationId(); + const { responseBody, statusCode } = await getLettersWithRetry( + request, + baseUrl, + headers, + { + waitForVisibilityTimeout: false, }, - }); - expect(response.status()).toBe(500); - const responseBody = await response.json(); + ); + expect(statusCode).toBe(500); + if (!isErrorResponse(responseBody)) { + throw new Error("Expected ErrorResponse body for 500 status"); + } expect(responseBody.errors[0].code).toBe("NOTIFY_INTERNAL_SERVER_ERROR"); expect(responseBody.errors[0].detail).toBe("Unexpected error"); }); @@ -65,15 +78,17 @@ test.describe("API Gateway Tests To Get List Of Pending Letters", () => { test("GET /letters with invalid query param return 400", async ({ request, }) => { - const header = createValidRequestHeaders(); - const response = await request.get(`${baseUrl}/${SUPPLIER_LETTERS}`, { - headers: header, - params: { - limit: "?", + const headers = createValidRequestHeaders(); + const { responseBody, statusCode } = await getLettersWithRetry( + request, + baseUrl, + headers, + { + lettersLimit: "?", + waitForVisibilityTimeout: false, }, - }); - expect(response.status()).toBe(400); - const responseBody = await response.json(); + ); + expect(statusCode).toBe(400); expect(responseBody).toMatchObject({ errors: [ { diff --git a/tests/component-tests/integration-tests/urgent-letter-priority.spec.ts b/tests/component-tests/integration-tests/urgent-letter-priority.spec.ts index c49a32d21..8b72dbbae 100644 --- a/tests/component-tests/integration-tests/urgent-letter-priority.spec.ts +++ b/tests/component-tests/integration-tests/urgent-letter-priority.spec.ts @@ -9,13 +9,12 @@ import { verifyIndexPositionOfLetterVariants, } from "tests/helpers/urgent-letter-priority-helper"; import { createValidRequestHeaders } from "tests/constants/request-headers"; -import { SUPPLIER_LETTERS } from "tests/constants/api-constants"; import { supplierDataSetup } from "tests/helpers/suppliers-setup-helper"; import { logger } from "tests/helpers/pino-logger"; import { - GetLettersResponse, - GetLettersResponseSchema, -} from "../../../lambdas/api-handler/src/contracts/letters"; + getLettersWithRetry, + isGetLettersResponse, +} from "tests/helpers/generate-fetch-test-data"; let baseUrl: string; @@ -46,19 +45,21 @@ test.describe("Urgent Letter Priority Tests", () => { await verifyAllocationLogsContainPriority(urgencyNineLetterIds, 9); await verifyAllocationLogsContainPriority(urgencyTenLetterIds, 10); - const header = createValidRequestHeaders(supplier); - const response = await request.get(`${baseUrl}/${SUPPLIER_LETTERS}`, { - headers: header, - }); + const headers = createValidRequestHeaders(supplier); + const { responseBody, statusCode } = await getLettersWithRetry( + request, + baseUrl, + headers, + ); - expect(response.status()).toBe(200); - const responseBody = await response.json(); - expect(responseBody.data.length).toBeGreaterThanOrEqual(1); + expect(statusCode).toBe(200); + if (!isGetLettersResponse(responseBody)) { + throw new Error("Expected GetLettersResponse body for 200 status"); + } - const getLettersResponse: GetLettersResponse = - GetLettersResponseSchema.parse(responseBody); + expect(responseBody.data.length).toBeGreaterThanOrEqual(1); - const letterIds = getLettersResponse.data.map((letter) => letter.id); + const letterIds = responseBody.data.map((letter) => letter.id); verifyIndexPositionOfLetterVariants( letterIds, diff --git a/tests/component-tests/letterQueue-tests/queue-operations.spec.ts b/tests/component-tests/letterQueue-tests/queue-operations.spec.ts index 1810f273c..ef96ef586 100644 --- a/tests/component-tests/letterQueue-tests/queue-operations.spec.ts +++ b/tests/component-tests/letterQueue-tests/queue-operations.spec.ts @@ -11,11 +11,16 @@ import { supplierIdFromSupplierAllocatorLog, } from "tests/helpers/aws-cloudwatch-helper"; import getRestApiGatewayBaseUrl from "tests/helpers/aws-gateway-helper"; -import { SUPPLIER_LETTERS } from "tests/constants/api-constants"; +import { + SUPPLIER_LETTERS, + VISIBILITY_TIMEOUT_SECONDS, +} from "tests/constants/api-constants"; import { supplierDataSetup } from "tests/helpers/suppliers-setup-helper"; import { checkLetterQueueTable, getLetterFromQueueById, + getLettersWithRetry, + isGetLettersResponse, } from "tests/helpers/generate-fetch-test-data"; import { createValidRequestHeaders } from "../../constants/request-headers"; import { @@ -132,17 +137,21 @@ test.describe("Letter Queue Tests", () => { ); // call get letters endpoint which should update the visibility timestamp - const header = createValidRequestHeaders(supplierId); - const getLettersResponse = await request.get( - `${baseUrl}/${SUPPLIER_LETTERS}`, - { - headers: header, - }, + const headers = createValidRequestHeaders(supplierId); + const { responseBody, statusCode } = await getLettersWithRetry( + request, + baseUrl, + headers, ); - expect(getLettersResponse.status()).toBe(200); + expect(statusCode).toBe(200); + if (!isGetLettersResponse(responseBody)) { + throw new Error("Expected GetLettersResponse body for 200 status"); + } + expect(responseBody.data.length).toBeGreaterThanOrEqual(1); + const currentTimeWithTimeOut = Math.floor( - (Date.now() + 5 * 60 * 1000) / 1000, + (Date.now() + VISIBILITY_TIMEOUT_SECONDS * 1000) / 1000, ); logger.info( @@ -158,15 +167,15 @@ test.describe("Letter Queue Tests", () => { Math.abs(visibilityTimestampAfterGet - currentTimeWithTimeOut), ).toBeLessThanOrEqual(1); - const getLettersWithInVisibility = await request.get( - `${baseUrl}/${SUPPLIER_LETTERS}`, - { - headers: header, - }, - ); + const { responseBody: secondResponseBody, statusCode: secondStatusCode } = + await getLettersWithRetry(request, baseUrl, headers, { + waitForVisibilityTimeout: false, + }); - expect(getLettersWithInVisibility.status()).toBe(200); - const responseBody = await getLettersWithInVisibility.json(); - expect(responseBody.data).toHaveLength(0); + expect(secondStatusCode).toBe(200); + if (!isGetLettersResponse(secondResponseBody)) { + throw new Error("Expected GetLettersResponse body for 200 status"); + } + expect(secondResponseBody.data).toHaveLength(0); }); }); diff --git a/tests/helpers/generate-fetch-test-data.ts b/tests/helpers/generate-fetch-test-data.ts index 60aa046ae..3f0e4e5ab 100644 --- a/tests/helpers/generate-fetch-test-data.ts +++ b/tests/helpers/generate-fetch-test-data.ts @@ -5,15 +5,25 @@ import { GetCommand, QueryCommand, } from "@aws-sdk/lib-dynamodb"; +import { APIRequestContext } from "@playwright/test"; import z from "zod"; import { + AWS_ACCOUNT_ID, + GET_LETTERS_MAX_RETRIES, LETTERQUEUE_TABLENAME, LETTERSTABLENAME, SUPPLIERTABLENAME, + SUPPLIER_LETTERS, + VISIBILITY_TIMEOUT_SECONDS, envName, } from "../constants/api-constants"; import { createSupplierData, runCreateLetter } from "./pnpm-helpers"; import { logger } from "./pino-logger"; +import { + GetLettersResponse, + GetLettersResponseSchema, +} from "../../lambdas/api-handler/src/contracts/letters"; +import { ErrorResponse } from "../../lambdas/api-handler/src/contracts/errors"; const ddb = new DynamoDBClient({}); const docClient = DynamoDBDocumentClient.from(ddb); @@ -53,7 +63,7 @@ export async function createTestData( filter: "nhs-notify-supplier-api-letter-test-data-utility", supplierId, environment: envName, - awsAccountId: "820178564574", + awsAccountId: AWS_ACCOUNT_ID, groupId: "TestGroupID", specificationId: "TestSpecificationID", status: "PENDING", @@ -92,6 +102,111 @@ const delay = (ms: number) => setTimeout(resolve, ms); }); +type FetchLettersWithRetryOptions = { + lettersLimit?: string; + waitForVisibilityTimeout?: boolean; +}; + +type GetLettersResponseBody = + | GetLettersResponse + | ErrorResponse + | Record; + +type FetchLettersWithRetryResult = { + statusCode: number; + responseBody: GetLettersResponseBody; +}; + +export function isGetLettersResponse( + responseBody: GetLettersResponseBody, +): responseBody is GetLettersResponse { + return GetLettersResponseSchema.safeParse(responseBody).success; +} + +export function isErrorResponse( + responseBody: GetLettersResponseBody, +): responseBody is ErrorResponse { + return ( + typeof responseBody === "object" && + Array.isArray((responseBody as ErrorResponse).errors) + ); +} + +function parseGetLettersResponseBody( + parsedBody: unknown, +): GetLettersResponseBody { + const parsedGetLettersResponse = + GetLettersResponseSchema.safeParse(parsedBody); + if (parsedGetLettersResponse.success) { + return parsedGetLettersResponse.data; + } + + if (isErrorResponse(parsedBody as GetLettersResponseBody)) { + return parsedBody as ErrorResponse; + } + + return parsedBody as Record; +} + +function shouldRetryGetLettersRequest( + waitForVisibilityTimeout: boolean, + statusCode: number, + responseBody: GetLettersResponseBody, +): boolean { + const dataIsEmpty = + isGetLettersResponse(responseBody) && + Array.isArray(responseBody.data) && + responseBody.data.length === 0; + + return waitForVisibilityTimeout && statusCode === 200 && dataIsEmpty; +} + +export async function getLettersWithRetry( + request: APIRequestContext, + baseUrl: string, + headers: Record, + options?: FetchLettersWithRetryOptions, +): Promise { + const limit = options?.lettersLimit; + const waitForVisibilityTimeout = options?.waitForVisibilityTimeout ?? true; + + const executeGetLettersRequest = + limit === undefined + ? () => + request.get(`${baseUrl}/${SUPPLIER_LETTERS}`, { + headers, + }) + : () => + request.get(`${baseUrl}/${SUPPLIER_LETTERS}`, { + headers, + params: { + limit, + }, + }); + + for (let attempt = 0; attempt <= GET_LETTERS_MAX_RETRIES; attempt++) { + const response = await executeGetLettersRequest(); + const statusCode = response.status(); + + const parsedBody = (await response.json()) as unknown; + const responseBody = parseGetLettersResponseBody(parsedBody); + + const shouldRetry = shouldRetryGetLettersRequest( + waitForVisibilityTimeout, + statusCode, + responseBody, + ); + + if (!shouldRetry || attempt === GET_LETTERS_MAX_RETRIES) { + return { statusCode, responseBody }; + } + + await delay(VISIBILITY_TIMEOUT_SECONDS * 1000); + } + + throw new Error("Unexpectedly exhausted GET /letters retries"); +} + export async function waitForLetterStatus( supplierId: string, id: string, From 78c0f87d16dd2e2cc6523c82cd6af1ea8f3cf4ff Mon Sep 17 00:00:00 2001 From: Francisco Videira Date: Thu, 21 May 2026 18:30:32 +0000 Subject: [PATCH 17/19] e2e: map account IDs by target group and block production test execution --- tests/constants/api-constants.ts | 40 ++++++++++++++++++- tests/e2e-tests/lib/constants.py | 31 ++++++++++++++ tests/e2e-tests/lib/letters.py | 10 ++++- .../send-prepared-letter-request.spec.ts | 6 +-- 4 files changed, 80 insertions(+), 7 deletions(-) diff --git a/tests/constants/api-constants.ts b/tests/constants/api-constants.ts index 0afe02ca3..9d66106c2 100644 --- a/tests/constants/api-constants.ts +++ b/tests/constants/api-constants.ts @@ -8,11 +8,47 @@ export const LETTERSTABLENAME = `nhs-${envName}-supapi-letters`; export const SUPPLIERID = "supplier1"; export const MI_ENDPOINT = "mi"; export const SUPPLIERTABLENAME = `nhs-${envName}-supapi-suppliers`; -export const UPSERT_LETTER_LAMBDA_ARN = `arn:aws:lambda:eu-west-2:820178564574:function:nhs-${envName}-supapi-upsertletter`; export const DATA = "data"; export const EVENT_SUBSCRIPTION_TOPIC_NAME = `nhs-${envName}-supapi-eventsub`; -export const AWS_ACCOUNT_ID = process.env.AWS_ACCOUNT_ID ?? "820178564574"; + +export const DEFAULT_TARGET_ACCOUNT_GROUP = "nhs-notify-supplier-api-dev"; +export const PROD_TARGET_ACCOUNT_GROUP = "nhs-notify-supplier-api-prod"; +export const TARGET_ACCOUNT_GROUP = + process.env.TARGET_ACCOUNT_GROUP ?? DEFAULT_TARGET_ACCOUNT_GROUP; + +export const ACCOUNT_GROUP_TO_AWS_ACCOUNT_ID = new Map([ + ["nhs-notify-supplier-api-dev", "820178564574"], + ["nhs-notify-supplier-api-nonprod", "885964308133"], +]); + +function resolveAwsAccountId(): string { + if (TARGET_ACCOUNT_GROUP === PROD_TARGET_ACCOUNT_GROUP) { + throw new Error( + `TARGET_ACCOUNT_GROUP='${TARGET_ACCOUNT_GROUP}' points to production. Test execution against production is blocked.`, + ); + } + + const mappedAccountId = + ACCOUNT_GROUP_TO_AWS_ACCOUNT_ID.get(TARGET_ACCOUNT_GROUP); + if (mappedAccountId) { + return mappedAccountId; + } + + throw new Error( + `No AWS account mapping configured for TARGET_ACCOUNT_GROUP='${TARGET_ACCOUNT_GROUP}'. Add a mapping in tests/constants/api-constants.ts.`, + ); +} + +export const AWS_ACCOUNT_ID = resolveAwsAccountId(); +export const UPSERT_LETTER_LAMBDA_ARN = `arn:aws:lambda:eu-west-2:${AWS_ACCOUNT_ID}:function:nhs-${envName}-supapi-upsertletter`; export const EVENT_SUBSCRIPTION_TOPIC_ARN = process.env.EVENT_SUBSCRIPTION_TOPIC_ARN ?? `arn:aws:sns:${AWS_REGION}:${AWS_ACCOUNT_ID}:${EVENT_SUBSCRIPTION_TOPIC_NAME}`; export const LETTERQUEUE_TABLENAME = `nhs-${envName}-supapi-letter-queue`; +export const GET_LETTERS_MAX_RETRIES = 3; +export const DEV_VISIBILITY_TIMEOUT_SECONDS = 10; +export const DEFAULT_VISIBILITY_TIMEOUT_SECONDS = 300; +export const VISIBILITY_TIMEOUT_SECONDS = + TARGET_ACCOUNT_GROUP === DEFAULT_TARGET_ACCOUNT_GROUP + ? DEV_VISIBILITY_TIMEOUT_SECONDS + : DEFAULT_VISIBILITY_TIMEOUT_SECONDS; diff --git a/tests/e2e-tests/lib/constants.py b/tests/e2e-tests/lib/constants.py index 79822086f..2d9fb7ea7 100644 --- a/tests/e2e-tests/lib/constants.py +++ b/tests/e2e-tests/lib/constants.py @@ -1,5 +1,36 @@ +import os + VALID_ENDPOINT_LETTERS= ["/letters"] METHODS = ["get", "post"] DEFAULT_CONTENT_TYPE = "application/vnd.api+json" LETTERS_ENDPOINT = "/letters" MI_ENDPOINT = "/mi" + +DEFAULT_TARGET_ACCOUNT_GROUP = "nhs-notify-supplier-api-dev" +PROD_TARGET_ACCOUNT_GROUP = "nhs-notify-supplier-api-prod" +ACCOUNT_GROUP_TO_AWS_ACCOUNT_ID = { + "nhs-notify-supplier-api-dev": "820178564574", + "nhs-notify-supplier-api-nonprod": "885964308133", +} + + +def resolve_aws_account_id() -> str: + target_account_group = os.environ.get( + "TARGET_ACCOUNT_GROUP", + DEFAULT_TARGET_ACCOUNT_GROUP, + ) + if target_account_group == PROD_TARGET_ACCOUNT_GROUP: + raise RuntimeError( + f"TARGET_ACCOUNT_GROUP='{target_account_group}' points to production. " + "Test execution against production is blocked." + ) + + mapped_account_id = ACCOUNT_GROUP_TO_AWS_ACCOUNT_ID.get(target_account_group) + if mapped_account_id: + return mapped_account_id + + raise RuntimeError( + "No AWS account mapping configured for " + f"TARGET_ACCOUNT_GROUP='{target_account_group}'. " + "Add a mapping in tests/e2e-tests/lib/constants.py." + ) diff --git a/tests/e2e-tests/lib/letters.py b/tests/e2e-tests/lib/letters.py index ecadca468..700541adb 100644 --- a/tests/e2e-tests/lib/letters.py +++ b/tests/e2e-tests/lib/letters.py @@ -4,6 +4,7 @@ import time import json import requests +from lib.constants import resolve_aws_account_id from lib.errorhandler import ErrorHandler @@ -21,7 +22,7 @@ def create_test_data(count: int = 10) -> list[str]: Returns a list of letter IDs created by the CLI. """ environment = os.environ.get("TARGET_ENVIRONMENT", "main") - aws_account_id = os.environ.get("AWS_ACCOUNT_ID", "820178564574") + aws_account_id = resolve_aws_account_id() cmd = [ "npm", @@ -77,9 +78,14 @@ def get_pending_letter_ids( retries: int = 5, ) -> list: """Injects the given number of pending letters as test data, then waits for them to become - visible via the letters endpoint. Retries to account for other tests running in parallel stealing the letters + visible via the getLetters endpoint. + + Because the getLetters endpoint increases the visibility timeout, if it is called immediately again before the letter's visibility timeout expires, the same letter will not be returned in the response. + + Retries to account for other tests running in parallel stealing the letters. Returns a list of letter ID strings. + Raises TimeoutError if the expected number of pending letters do not appear within the timeout period. """ diff --git a/tests/performance/testCases/send-prepared-letter-request.spec.ts b/tests/performance/testCases/send-prepared-letter-request.spec.ts index 4381105f6..c678cdaff 100644 --- a/tests/performance/testCases/send-prepared-letter-request.spec.ts +++ b/tests/performance/testCases/send-prepared-letter-request.spec.ts @@ -6,7 +6,7 @@ import { expect, test } from "@playwright/test"; import { snsClient } from "tests/helpers/aws-sns-helper"; import { retrieveKinesisRecordsAtTimestamp } from "tests/helpers/aws-kinesis-helper"; import { logger } from "tests/helpers/pino-logger"; -import { envName } from "tests/constants/api-constants"; +import { AWS_ACCOUNT_ID, envName } from "tests/constants/api-constants"; import { randomUUID } from "node:crypto"; import PREPARED_LETTER from "../../resources/prepared-letter.json"; @@ -15,8 +15,8 @@ test.describe("Performance test checking how long it takes letter requests from const MESSAGES_TO_SEND = 2500; const FIVE_MINUTES = 1000 * 60 * 5; const BATCH_SIZE = 10; - const SNS_ARN = `arn:aws:sns:eu-west-2:820178564574:nhs-${envName}-supapi-eventsub`; - const KINESIS_STREAM_ARN = `arn:aws:kinesis:eu-west-2:820178564574:stream/nhs-${envName}-supapi-letter-change-stream`; + const SNS_ARN = `arn:aws:sns:eu-west-2:${AWS_ACCOUNT_ID}:nhs-${envName}-supapi-eventsub`; + const KINESIS_STREAM_ARN = `arn:aws:kinesis:eu-west-2:${AWS_ACCOUNT_ID}:stream/nhs-${envName}-supapi-letter-change-stream`; test.setTimeout(FIVE_MINUTES); const startTime = Date.now(); From 11691330b0ca1ba99fc8b6a345a236d5269ad7c0 Mon Sep 17 00:00:00 2001 From: Francisco Videira Date: Thu, 21 May 2026 18:30:59 +0000 Subject: [PATCH 18/19] update env template --- .env.template | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/.env.template b/.env.template index 6aed7e69a..6bc94ea30 100644 --- a/.env.template +++ b/.env.template @@ -5,12 +5,18 @@ GITHUB_TOKEN= # nhs-notify-supplier--internal-dev--nhs-notify-supplier-PR-XX PROXY_NAME= -# APIM env to run tests against, other options are: ref, int, prod +# APIM env to run e2e tests against, other options are: ref, int, prod API_ENVIRONMENT=internal-dev -# 820178564574 Supplier Dev is the default if removed -# See AWS access portal for others -AWS_ACCOUNT_ID=820178564574 +# Used for component and e2e tests +# Account group controls default account id mapping for tests. +# If omitted, tests default to dev: +# TARGET_ACCOUNT_GROUP=nhs-notify-supplier-api-dev +# Mapping used by tests: +# nhs-notify-supplier-api-dev -> 820178564574 +# nhs-notify-supplier-api-nonprod -> 885964308133 +# nhs-notify-supplier-api-prod -> blocked (tests are intentionally disabled for prod) +TARGET_ACCOUNT_GROUP=nhs-notify-supplier-api-dev # Resource namespace used to resolve AWS resource names for tests (main, pr123) # remove if needs to run against main @@ -36,7 +42,3 @@ export NON_PROD_PRIVATE_KEY=xxx export INTEGRATION_PRIVATE_KEY=xxx # private key path used to generate authentication for tests ran against the prod environment export PRODUCTION_PRIVATE_KEY=xxx - -# E2E Test Variables -# ======== -# To set variables for running E2E tests locally see tests/e2e-tests/README.md From f095fcbbab0b073f348eed8294140e9ecd9075ec Mon Sep 17 00:00:00 2001 From: Francisco Videira Date: Fri, 22 May 2026 16:50:10 +0000 Subject: [PATCH 19/19] env specific lambda runtime env vars --- .../terraform/components/api/README.md | 5 +++ .../terraform/components/api/locals.tf | 34 ++++++++----------- .../terraform/components/api/variables.tf | 30 ++++++++++++++++ 3 files changed, 49 insertions(+), 20 deletions(-) diff --git a/infrastructure/terraform/components/api/README.md b/infrastructure/terraform/components/api/README.md index 493cbad2a..3a6e442c4 100644 --- a/infrastructure/terraform/components/api/README.md +++ b/infrastructure/terraform/components/api/README.md @@ -19,6 +19,7 @@ No requirements. | [csoc\_log\_forwarding](#input\_csoc\_log\_forwarding) | Enable forwarding of API Gateway logs to CSOC | `bool` | `true` | no | | [default\_tags](#input\_default\_tags) | A map of default tags to apply to all taggable resources within the component | `map(string)` | `{}` | no | | [disable\_gateway\_execute\_endpoint](#input\_disable\_gateway\_execute\_endpoint) | Disable the execution endpoint for the API Gateway | `bool` | `true` | no | +| [download\_url\_ttl\_seconds](#input\_download\_url\_ttl\_seconds) | TTL in seconds for generated download URLs | `number` | `60` | no | | [enable\_alarms](#input\_enable\_alarms) | Enable CloudWatch alarms for this deployed environment | `bool` | `true` | no | | [enable\_api\_data\_trace](#input\_enable\_api\_data\_trace) | Enable API Gateway data trace logging | `bool` | `false` | no | | [enable\_backups](#input\_enable\_backups) | Enable backups | `bool` | `false` | no | @@ -36,11 +37,15 @@ No requirements. | [group](#input\_group) | The account group short-name | `string` | n/a | yes | | [kms\_deletion\_window](#input\_kms\_deletion\_window) | When a kms key is deleted, how long should it wait in the pending deletion state? | `string` | `"30"` | no | | [letter\_event\_source](#input\_letter\_event\_source) | Source value to use for the letter status event updates | `string` | `"/data-plane/supplier-api/nhs-supplier-api-prod/main/update-status"` | no | +| [letter\_queue\_ttl\_hours](#input\_letter\_queue\_ttl\_hours) | TTL in hours for letter queue records | `number` | `168` | no | +| [letter\_queue\_visibility\_timeout](#input\_letter\_queue\_visibility\_timeout) | Visibility timeout in seconds for processing queued letter updates | `number` | `300` | no | | [letter\_table\_ttl\_hours](#input\_letter\_table\_ttl\_hours) | Number of hours to set as TTL on letters table | `number` | `24` | no | +| [letter\_ttl\_hours](#input\_letter\_ttl\_hours) | TTL in hours for letter records | `number` | `12960` | no | | [log\_level](#input\_log\_level) | The log level to be used in lambda functions within the component. Any log with a lower severity than the configured value will not be logged: https://docs.python.org/3/library/logging.html#levels | `string` | `"INFO"` | no | | [log\_retention\_in\_days](#input\_log\_retention\_in\_days) | The retention period in days for the Cloudwatch Logs events to be retained, default of 0 is indefinite | `number` | `0` | no | | [manually\_configure\_mtls\_truststore](#input\_manually\_configure\_mtls\_truststore) | Manually manage the truststore used for API Gateway mTLS (e.g. for prod environment) | `bool` | `false` | no | | [max\_get\_limit](#input\_max\_get\_limit) | Default limit to apply to GET requests that support pagination | `number` | `2500` | no | +| [mi\_ttl\_hours](#input\_mi\_ttl\_hours) | TTL in hours for MI records | `number` | `2160` | no | | [parent\_acct\_environment](#input\_parent\_acct\_environment) | Name of the environment responsible for the acct resources used, affects things like DNS zone. Useful for named dev environments | `string` | `"main"` | no | | [project](#input\_project) | The name of the tfscaffold project | `string` | n/a | yes | | [region](#input\_region) | The AWS Region | `string` | n/a | yes | diff --git a/infrastructure/terraform/components/api/locals.tf b/infrastructure/terraform/components/api/locals.tf index 9984e0aad..5a37f1f12 100644 --- a/infrastructure/terraform/components/api/locals.tf +++ b/infrastructure/terraform/components/api/locals.tf @@ -22,26 +22,20 @@ locals { destination_arn = "arn:aws:logs:${var.region}:${var.shared_infra_account_id}:destination:nhs-main-obs-firehose-logs" common_lambda_env_vars = { - APIM_CORRELATION_HEADER = "nhsd-correlation-id", - DOWNLOAD_URL_TTL_SECONDS = 60, - EVENT_SOURCE = "/data-plane/supplier-api/${var.group}/${var.environment}/letters", - LETTER_TTL_HOURS = 12960, # 18 months * 30 days * 24 hours - LETTER_QUEUE_TABLE_NAME = aws_dynamodb_table.letter_queue.name, - LETTER_QUEUE_TTL_HOURS = 168, # 7 days * 24 hours - LETTER_QUEUE_VISIBILITY_TIMEOUT = lookup( - { - nhs-notify-supplier-api-dev = 10, # 10 seconds for development/testing - }, - var.group, - 300, # Default to 5 minutes for other groups (nonprod and prod) - ), - LETTERS_TABLE_NAME = aws_dynamodb_table.letters.name, - MI_TABLE_NAME = aws_dynamodb_table.mi.name, - MI_TTL_HOURS = 2160, # 90 days * 24 hours - SNS_TOPIC_ARN = "${module.eventsub.sns_topic.arn}", - SUPPLIER_CONFIG_TABLE_NAME = aws_dynamodb_table.supplier-configuration.name, - SUPPLIER_QUOTAS_TABLE_NAME = aws_dynamodb_table.supplier-quotas.name, - SUPPLIER_ID_HEADER = "nhsd-supplier-id", + APIM_CORRELATION_HEADER = "nhsd-correlation-id", + DOWNLOAD_URL_TTL_SECONDS = var.download_url_ttl_seconds, + EVENT_SOURCE = "/data-plane/supplier-api/${var.group}/${var.environment}/letters", + LETTER_TTL_HOURS = var.letter_ttl_hours, + LETTER_QUEUE_TABLE_NAME = aws_dynamodb_table.letter_queue.name, + LETTER_QUEUE_TTL_HOURS = var.letter_queue_ttl_hours, + LETTER_QUEUE_VISIBILITY_TIMEOUT = var.letter_queue_visibility_timeout, + LETTERS_TABLE_NAME = aws_dynamodb_table.letters.name, + MI_TABLE_NAME = aws_dynamodb_table.mi.name, + MI_TTL_HOURS = var.mi_ttl_hours, + SNS_TOPIC_ARN = "${module.eventsub.sns_topic.arn}", + SUPPLIER_CONFIG_TABLE_NAME = aws_dynamodb_table.supplier-configuration.name, + SUPPLIER_QUOTAS_TABLE_NAME = aws_dynamodb_table.supplier-quotas.name, + SUPPLIER_ID_HEADER = "nhsd-supplier-id", } core_pdf_bucket_arn = "arn:aws:s3:::comms-${var.core_account_id}-eu-west-2-${var.core_environment}-api-stg-pdf-pipeline" diff --git a/infrastructure/terraform/components/api/variables.tf b/infrastructure/terraform/components/api/variables.tf index bac5b8361..efea09a15 100644 --- a/infrastructure/terraform/components/api/variables.tf +++ b/infrastructure/terraform/components/api/variables.tf @@ -111,6 +111,36 @@ variable "max_get_limit" { default = 2500 } +variable "download_url_ttl_seconds" { + type = number + description = "TTL in seconds for generated download URLs" + default = 60 +} + +variable "letter_ttl_hours" { + type = number + description = "TTL in hours for letter records" + default = 12960 +} + +variable "letter_queue_ttl_hours" { + type = number + description = "TTL in hours for letter queue records" + default = 168 +} + +variable "letter_queue_visibility_timeout" { + type = number + description = "Visibility timeout in seconds for processing queued letter updates" + default = 300 +} + +variable "mi_ttl_hours" { + type = number + description = "TTL in hours for MI records" + default = 2160 +} + variable "parent_acct_environment" { type = string description = "Name of the environment responsible for the acct resources used, affects things like DNS zone. Useful for named dev environments"