diff --git a/.github/workflows/pr-env-deploy.yml b/.github/workflows/pr-env-deploy.yml index 7a0f13128..76e27080f 100644 --- a/.github/workflows/pr-env-deploy.yml +++ b/.github/workflows/pr-env-deploy.yml @@ -300,10 +300,10 @@ jobs: run: make test-performance-prepare TF_WORKSPACE_NAME=${{ needs.set-environment-id.outputs.environment_id }} - name: Run Performance Test - Baseline - run: make test-performance-baseline HOST=${{ needs.set-environment-id.outputs.environment_id }}.api.record-locator.dev.national.nhs.uk ENV_TYPE=dev + run: make test-performance-baseline-internal HOST=${{ needs.set-environment-id.outputs.environment_id }}.api.record-locator.dev.national.nhs.uk ENV_TYPE=dev - name: Run Performance Test - Stress - run: make test-performance-stress HOST=${{ needs.set-environment-id.outputs.environment_id }}.api.record-locator.dev.national.nhs.uk ENV_TYPE=dev + run: make test-performance-stress-internal HOST=${{ needs.set-environment-id.outputs.environment_id }}.api.record-locator.dev.national.nhs.uk ENV_TYPE=dev - name: Process Performance Test Outputs run: make test-performance-output diff --git a/Makefile b/Makefile index 603f99357..39e7c07fe 100644 --- a/Makefile +++ b/Makefile @@ -156,20 +156,58 @@ test-performance-prepare: mkdir -p $(DIST_PATH) PYTHONPATH=. poetry run python tests/performance/environment.py setup $(TF_WORKSPACE_NAME) -test-performance: check-warn test-performance-baseline test-performance-stress ## Run the performance tests +test-performance-internal: check-warn test-performance-baseline-internal test-performance-stress-internal ## Run the performance tests against the internal access points -test-performance-baseline: - @echo "Running consumer performance baseline test" - k6 run --out csv=$(DIST_PATH)/consumer-baseline.csv tests/performance/consumer/baseline.js -e HOST=$(HOST) -e ENV_TYPE=$(ENV_TYPE) - -test-performance-stress: - @echo "Running consumer performance stress test" +test-performance-baseline-internal: check-warn ## Run the performance baseline tests for the internal access points + @echo "Running internal consumer performance baseline test" + TEST_CONNECT_MODE=internal \ + TEST_STACK_DOMAIN=$(shell terraform -chdir=terraform/infrastructure output -raw domain 2>/dev/null) \ + k6 run --out csv=$(DIST_PATH)/consumer-baseline.csv tests/performance/consumer/baseline.js -e HOST=$(HOST) -e ENV_TYPE=$(ENV_TYPE) + +test-performance-baseline-public: check-warn ## Run the baseline performance tests for the external access points + @echo "Fetching public mode configuration and bearer token..." + @CONFIG_FILE=$$(mktemp /tmp/perf_config_XXXXXX); \ + trap "rm -f $$CONFIG_FILE" EXIT; \ + PYTHONPATH=. python3 tests/performance/get_test_config.py $(ENV_TYPE) 2>&1 | tail -n 1 > $$CONFIG_FILE; \ + PUBLIC_BASE_URL=$$(jq -r '.public_base_url' $$CONFIG_FILE); \ + echo "Running consumer performance baseline test against the external access points"; \ + TEST_CONNECT_MODE=public \ + TEST_PUBLIC_BASE_URL=$$PUBLIC_BASE_URL \ + TEST_CONFIG_FILE=$$CONFIG_FILE \ + k6 run --out csv=$(DIST_PATH)/consumer-baseline-public.csv tests/performance/consumer/baseline.js -e ENV_TYPE=$(ENV_TYPE) + +test-performance-stress-internal: ## Run the performance stress tests for the internal access points + @echo "Running internal consumer performance stress test" k6 run --out csv=$(DIST_PATH)/consumer-stress.csv tests/performance/consumer/stress.js -e HOST=$(HOST) -e ENV_TYPE=$(ENV_TYPE) -test-performance-soak: - @echo "Running consumer performance soak test" +test-performance-stress-public: check-warn ## Run the stress performance tests for the external access points + @echo "Fetching public mode configuration and bearer token..." + @CONFIG_FILE=$$(mktemp /tmp/perf_config_XXXXXX); \ + trap "rm -f $$CONFIG_FILE" EXIT; \ + PYTHONPATH=. python3 tests/performance/get_test_config.py $(ENV_TYPE) 2>&1 | tail -n 1 > $$CONFIG_FILE; \ + PUBLIC_BASE_URL=$$(jq -r '.public_base_url' $$CONFIG_FILE); \ + echo "Running consumer performance stress test against the external access points"; \ + TEST_CONNECT_MODE=public \ + TEST_PUBLIC_BASE_URL=$$PUBLIC_BASE_URL \ + TEST_CONFIG_FILE=$$CONFIG_FILE \ + k6 run --out csv=$(DIST_PATH)/consumer-stress-public.csv tests/performance/consumer/stress.js -e ENV_TYPE=$(ENV_TYPE) + +test-performance-soak-internal: + @echo "Running internal consumer performance soak test" k6 run --out csv=$(DIST_PATH)/consumer-soak.csv tests/performance/consumer/soak.js -e HOST=$(HOST) -e ENV_TYPE=$(ENV_TYPE) +test-performance-soak-public: check-warn ## Run the soak performance tests for the external access points + @echo "Fetching public mode configuration and bearer token..." + @CONFIG_FILE=$$(mktemp /tmp/perf_config_XXXXXX); \ + trap "rm -f $$CONFIG_FILE" EXIT; \ + PYTHONPATH=. python3 tests/performance/get_test_config.py $(ENV_TYPE) 2>&1 | tail -n 1 > $$CONFIG_FILE; \ + PUBLIC_BASE_URL=$$(jq -r '.public_base_url' $$CONFIG_FILE); \ + echo "Running consumer performance soak test against the external access points"; \ + TEST_CONNECT_MODE=public \ + TEST_PUBLIC_BASE_URL=$$PUBLIC_BASE_URL \ + TEST_CONFIG_FILE=$$CONFIG_FILE \ + k6 run --out csv=$(DIST_PATH)/consumer-soak-public.csv tests/performance/consumer/soak.js -e ENV_TYPE=$(ENV_TYPE) + test-performance-output: ## Process outputs from the performance tests @echo "Processing performance test outputs" poetry run python tests/performance/process_results.py baseline $(DIST_PATH)/consumer-baseline.csv @@ -273,15 +311,49 @@ perftest-prepare: ## Prepare input files for producer & consumer perf tests # cp "${DIST_PATH}/nft/seed-pointers-extract-${PERFTEST_TABLE_NAME}.csv" "${DIST_PATH}/seed-pointers-extract.csv" PYTHONPATH=. poetry run python ./tests/performance/generate_producer_distributions.py -perftest-producer: ## Run producer perf tests +perftest-producer-internal: ## Run producer perf tests @echo "Running producer performance tests with HOST=$(PERFTEST_HOST) and ENV_TYPE=$(ENV_TYPE) and DIST_PATH=$(DIST_PATH)" k6 run tests/performance/producer/perftest.js -e HOST=$(PERFTEST_HOST) -e ENV_TYPE=$(ENV_TYPE) -e DIST_PATH=$(DIST_PATH) -perftest-consumer: ## Run consumer perf tests +perftest-producer-public: check-warn ## Run the producer perftests for the external access points + @echo "Fetching public mode configuration and bearer token..." + @CONFIG_FILE=$$(mktemp /tmp/perf_config_XXXXXX); \ + trap "rm -f $$CONFIG_FILE" EXIT; \ + PYTHONPATH=. poetry run python tests/performance/get_test_config.py $(ENV_TYPE) 2>&1 | tail -n 1 > $$CONFIG_FILE; \ + PUBLIC_BASE_URL=$$(jq -r '.public_base_url' $$CONFIG_FILE); \ + echo "Running public producer perftests with ENV_TYPE=$(ENV_TYPE) and DIST_PATH=$(DIST_PATH)"; \ + TEST_CONNECT_MODE=public \ + TEST_PUBLIC_BASE_URL=$$PUBLIC_BASE_URL \ + TEST_CONFIG_FILE=$$CONFIG_FILE \ + k6 run tests/performance/producer/perftest.js -e ENV_TYPE=$(ENV_TYPE) -e DIST_PATH=$(DIST_PATH) + +perftest-consumer-internal: @echo "Running consumer performance tests with HOST=$(PERFTEST_HOST) and ENV_TYPE=$(ENV_TYPE) and DIST_PATH=$(DIST_PATH)" k6 run tests/performance/consumer/perftest.js -e HOST=$(PERFTEST_HOST) -e ENV_TYPE=$(ENV_TYPE) -e DIST_PATH=$(DIST_PATH) -perftest-generate-pointer-table-extract: ## Refresh the perf test input files in s3. Can be expensive to run on large tables +perftest-consumer-public: check-warn ## Run the consumer perftests for the external access points + @echo "Fetching public mode configuration and bearer token..." + @CONFIG_FILE=$$(mktemp /tmp/perf_config_XXXXXX); \ + trap "rm -f $$CONFIG_FILE" EXIT; \ + PYTHONPATH=. poetry run python tests/performance/get_test_config.py $(ENV_TYPE) 2>&1 | tail -n 1 > $$CONFIG_FILE; \ + PUBLIC_BASE_URL=$$(jq -r '.public_base_url' $$CONFIG_FILE); \ + echo "Running public consumer perftests with ENV_TYPE=$(ENV_TYPE) and DIST_PATH=$(DIST_PATH)"; \ + TEST_CONNECT_MODE=public \ + TEST_PUBLIC_BASE_URL=$$PUBLIC_BASE_URL \ + TEST_CONFIG_FILE=$$CONFIG_FILE \ + k6 run tests/performance/consumer/perftest.js -e ENV_TYPE=$(ENV_TYPE) -e DIST_PATH=$(DIST_PATH) + +perftest-prep-generate-producer-data: + @echo "Generating producer reference with PERFTEST_TABLE_NAME=$(PERFTEST_TABLE_NAME) and DIST_PATH=$(DIST_PATH)" + mkdir -p $(DIST_PATH) + PYTHONPATH=. poetry run python tests/performance/perftest_environment.py generate_producer_data --output_dir="$(DIST_PATH)" + +perftest-prep-extract-consumer-data: + @echo "Generating consumer reference with PERFTEST_TABLE_NAME=$(PERFTEST_TABLE_NAME) and DIST_PATH=$(DIST_PATH)" + mkdir -p $(DIST_PATH) + PYTHONPATH=. poetry run python tests/performance/perftest_environment.py extract_consumer_data --output_dir="$(DIST_PATH)" + +perftest-generate-pointer-table-extract: @echo "Generating pointer table extract with PERFTEST_TABLE_NAME=$(PERFTEST_TABLE_NAME) and DIST_PATH=$(DIST_PATH)" rm -rf "${DIST_PATH}/nft" mkdir -p "${DIST_PATH}/nft" diff --git a/tests/performance/README.md b/tests/performance/README.md index 18b5ae450..42a90e0cb 100644 --- a/tests/performance/README.md +++ b/tests/performance/README.md @@ -50,7 +50,8 @@ You will need to generate pointer permissions the first time performance tests a ```sh # In project root make perftest-generate-permissions # makes a bunch of json permission files for test organisations -make build # will take all permissions & create nrlf_permissions.zip file +make get-s3-perms ENV=perftest # will take all permissions & create nrlf_permissions.zip file +make build # apply this new permissions zip file to your environment cd ./terraform/infrastructure @@ -71,9 +72,20 @@ make perftest-prepare PERFTEST_TABLE_NAME=nhsd-nrlf--perftest-baseline-pointers- ### Run tests +#### Internal mode + +```sh +make perftest-consumer-internal ENV_TYPE=perftest PERFTEST_HOST=perftest-1.perftest.record-locator.national.nhs.uk +make perftest-producer-internal ENV_TYPE=perftest PERFTEST_HOST=perftest-1.perftest.record-locator.national.nhs.uk +``` + +#### Public mode + +Via apigee proxies - most similar to a supplier + ```sh -make perftest-consumer ENV_TYPE=perftest PERFTEST_HOST=perftest-1.perftest.record-locator.national.nhs.uk -make perftest-producer ENV_TYPE=perftest PERFTEST_HOST=perftest-1.perftest.record-locator.national.nhs.uk +make perftest-consumer-public ENV=perftest +make perftest-producer-public ENV=perftest ``` ## Seed data diff --git a/tests/performance/constants.js b/tests/performance/constants.js index 8883876ad..4bb1aacf4 100644 --- a/tests/performance/constants.js +++ b/tests/performance/constants.js @@ -17,7 +17,7 @@ export const NHS_NUMBERS = REFERENCE_DATA["nhs_numbers"]; // filter only 736253001, 736253002, 1363501000000100, 861421000000109, 749001000000101 for now export const FILTERED_POINTER_TYPES = [ - "736253001", + // "736253001", "736253002", "1363501000000100", "861421000000109", diff --git a/tests/performance/consumer/client.js b/tests/performance/consumer/client.js index d8bc3cbd2..5220a2a74 100644 --- a/tests/performance/consumer/client.js +++ b/tests/performance/consumer/client.js @@ -1,32 +1,14 @@ +import { getHeaders, getFullUrl } from "../test-config.js"; import { + POINTER_TYPES, + CATEGORIES, NHS_NUMBERS, POINTER_IDS, - POINTER_TYPES, ODS_CODE, - CATEGORIES, } from "../constants.js"; import http from "k6/http"; import { check } from "k6"; -function getHeaders(odsCode = ODS_CODE) { - return { - "Content-Type": "application/fhir+json", - "X-Request-Id": "K6PerformanceTest", - "NHSD-Correlation-Id": "K6PerformanceTest", - "NHSD-Connection-Metadata": JSON.stringify({ - "nrl.ods-code": odsCode, - "nrl.pointer-types": POINTER_TYPES.map( - (type) => `http://snomed.info/sct|${type}` - ), - "nrl.app-id": "K6PerformanceTest", - }), - "NHSD-Client-RP-Details": JSON.stringify({ - "developer.app.name": "K6PerformanceTest", - "developer.app.id": "K6PerformanceTest", - }), - }; -} - function checkResponse(res) { const is_success = check(res, { "status is 200": (r) => r.status === 200 }); if (!is_success) { @@ -41,12 +23,11 @@ export function countDocumentReference() { const identifier = encodeURIComponent( `https://fhir.nhs.uk/Id/nhs-number|${nhsNumber}` ); - const res = http.get( - `https://${__ENV.HOST}/consumer/DocumentReference?_summary=count&subject:identifier=${identifier}`, - { - headers: getHeaders(), - } - ); + + const path = `/DocumentReference?_summary=count&subject:identifier=${identifier}`; + const res = http.get(getFullUrl(path, "consumer"), { + headers: getHeaders(ODS_CODE, "consumer"), + }); checkResponse(res); } @@ -54,12 +35,10 @@ export function readDocumentReference() { const choice = Math.floor(Math.random() * POINTER_IDS.length); const id = POINTER_IDS[choice]; - const res = http.get( - `https://${__ENV.HOST}/consumer/DocumentReference/${id}`, - { - headers: getHeaders(), - } - ); + const path = `/DocumentReference/${id}`; + const res = http.get(getFullUrl(path, "consumer"), { + headers: getHeaders(ODS_CODE, "consumer"), + }); checkResponse(res); } @@ -74,12 +53,10 @@ export function searchDocumentReference() { ); const type = encodeURIComponent(`http://snomed.info/sct|${pointer_type}`); - const res = http.get( - `https://${__ENV.HOST}/consumer/DocumentReference?subject:identifier=${identifier}&type=${type}`, - { - headers: getHeaders(), - } - ); + const path = `/DocumentReference?subject:identifier=${identifier}&type=${type}`; + const res = http.get(getFullUrl(path, "consumer"), { + headers: getHeaders(ODS_CODE, "consumer"), + }); checkResponse(res); } @@ -95,12 +72,10 @@ export function searchDocumentReferenceByCategory() { `http://snomed.info/sct|${randomCategory}` ); - const res = http.get( - `https://${__ENV.HOST}/consumer/DocumentReference?subject:identifier=${identifier}&category=${category}`, - { - headers: getHeaders(), - } - ); + const path = `/DocumentReference?subject:identifier=${identifier}&category=${category}`; + const res = http.get(getFullUrl(path, "consumer"), { + headers: getHeaders(ODS_CODE, "consumer"), + }); checkResponse(res); } @@ -114,13 +89,10 @@ export function searchPostDocumentReference() { type: `http://snomed.info/sct|${pointer_type}`, }); - const res = http.post( - `https://${__ENV.HOST}/consumer/DocumentReference/_search`, - body, - { - headers: getHeaders(), - } - ); + const path = `/DocumentReference/_search`; + const res = http.post(getFullUrl(path, "consumer"), body, { + headers: getHeaders(ODS_CODE, "consumer"), + }); checkResponse(res); } @@ -133,13 +105,10 @@ export function searchPostDocumentReferenceByCategory() { category: `http://snomed.info/sct|${category}`, }); - const res = http.post( - `https://${__ENV.HOST}/consumer/DocumentReference/_search`, - body, - { - headers: getHeaders(), - } - ); + const path = `/DocumentReference/_search`; + const res = http.post(getFullUrl(path, "consumer"), body, { + headers: getHeaders(ODS_CODE, "consumer"), + }); checkResponse(res); } @@ -150,12 +119,10 @@ export function countPostDocumentReference() { const body = JSON.stringify({ "subject:identifier": `https://fhir.nhs.uk/Id/nhs-number|${nhsNumber}`, }); - const res = http.post( - `https://${__ENV.HOST}/consumer/DocumentReference/_search?_summary=count`, - body, - { - headers: getHeaders(), - } - ); + + const path = `/DocumentReference/_search?_summary=count`; + const res = http.post(getFullUrl(path, "consumer"), body, { + headers: getHeaders(ODS_CODE, "consumer"), + }); checkResponse(res); } diff --git a/tests/performance/consumer/client_perftest.js b/tests/performance/consumer/client_perftest.js index 74d5fdb74..f32310e9f 100644 --- a/tests/performance/consumer/client_perftest.js +++ b/tests/performance/consumer/client_perftest.js @@ -2,6 +2,7 @@ import http from "k6/http"; import { check } from "k6"; import exec from "k6/execution"; import { CATEGORY_TYPE_GROUPS } from "../type-category-mappings.js"; +import { getHeaders, getFullUrl } from "../test-config.js"; const distPath = __ENV.DIST_PATH || "./dist"; const csvPath = `../../../${distPath}/nft/seed-pointers-extract.csv`; @@ -21,22 +22,6 @@ function getNextPointer() { return { pointer_id, pointer_type, nhs_number }; } -function getHeaders(odsCode) { - return { - "Content-Type": "application/fhir+json", - "X-Request-Id": `K6perftest-consumer-${exec.scenario.name}-${exec.vu.idInTest}-${exec.vu.iterationInScenario}`, - "NHSD-Correlation-Id": `K6perftest-consumer-${exec.scenario.name}-${exec.vu.idInTest}-${exec.vu.iterationInScenario}`, - "NHSD-Connection-Metadata": JSON.stringify({ - "nrl.ods-code": odsCode, - "nrl.app-id": "K6PerformanceTest", - }), - "NHSD-Client-RP-Details": JSON.stringify({ - "developer.app.name": "K6PerformanceTest", - "developer.app.id": "K6PerformanceTest", - }), - }; -} - function getCustodianFromPointerId(pointer_id) { // pointer_id format is "CUSTODIAN-XXXX" return pointer_id.split("-")[0]; @@ -62,26 +47,22 @@ export function countDocumentReference() { const identifier = encodeURIComponent( `https://fhir.nhs.uk/Id/nhs-number|${nhs_number}` ); + const path = `/DocumentReference?_summary=count&subject:identifier=${identifier}`; - const res = http.get( - `https://${__ENV.HOST}/consumer/DocumentReference?_summary=count&subject:identifier=${identifier}`, - { - headers: getHeaders(custodian), - } - ); + const res = http.get(getFullUrl(path, "consumer"), { + headers: getHeaders(custodian, "consumer"), + }); checkResponse(res); } export function readDocumentReference() { const { pointer_id } = getNextPointer(); const custodian = getCustodianFromPointerId(pointer_id); + const path = `/DocumentReference/${pointer_id}`; - const res = http.get( - `https://${__ENV.HOST}/consumer/DocumentReference/${pointer_id}`, - { - headers: getHeaders(custodian), - } - ); + const res = http.get(getFullUrl(path, "consumer"), { + headers: getHeaders(custodian, "consumer"), + }); checkResponse(res); } @@ -94,13 +75,11 @@ export function searchDocumentReference() { `https://fhir.nhs.uk/Id/nhs-number|${nhs_number}` ); const type = encodeURIComponent(`http://snomed.info/sct|${pointer_type}`); + const path = `/DocumentReference?subject:identifier=${identifier}&type=${type}`; - const res = http.get( - `https://${__ENV.HOST}/consumer/DocumentReference?subject:identifier=${identifier}&type=${type}`, - { - headers: getHeaders(custodian), - } - ); + const res = http.get(getFullUrl(path, "consumer"), { + headers: getHeaders(custodian, "consumer"), + }); checkResponse(res); } @@ -116,12 +95,10 @@ export function searchDocumentReferenceByCategory() { `http://snomed.info/sct|${category_code}` ); - const res = http.get( - `https://${__ENV.HOST}/consumer/DocumentReference?subject:identifier=${identifier}&category=${category}`, - { - headers: getHeaders(custodian), - } - ); + const path = `/DocumentReference?subject:identifier=${identifier}&category=${category}`; + const res = http.get(getFullUrl(path, "consumer"), { + headers: getHeaders(custodian, "consumer"), + }); checkResponse(res); } @@ -134,13 +111,11 @@ export function searchPostDocumentReference() { type: `http://snomed.info/sct|${pointer_type}`, }); - const res = http.post( - `https://${__ENV.HOST}/consumer/DocumentReference/_search`, - body, - { - headers: getHeaders(custodian), - } - ); + const path = `/DocumentReference/_search`; + + const res = http.post(getFullUrl(path, "consumer"), body, { + headers: getHeaders(custodian, "consumer"), + }); checkResponse(res); } @@ -154,13 +129,11 @@ export function searchPostDocumentReferenceByCategory() { category: `http://snomed.info/sct|${category_code}`, }); - const res = http.post( - `https://${__ENV.HOST}/consumer/DocumentReference/_search`, - body, - { - headers: getHeaders(custodian), - } - ); + const path = `/DocumentReference/_search`; + + const res = http.post(getFullUrl(path, "consumer"), body, { + headers: getHeaders(custodian, "consumer"), + }); checkResponse(res); } @@ -172,13 +145,10 @@ export function countPostDocumentReference() { "subject:identifier": `https://fhir.nhs.uk/Id/nhs-number|${nhs_number}`, }); - const res = http.post( - `https://${__ENV.HOST}/consumer/DocumentReference/_search?_summary=count`, - body, - { - headers: getHeaders(custodian), - } - ); + const path = `/DocumentReference/_search?_summary=count`; + const res = http.post(getFullUrl(path, "consumer"), body, { + headers: getHeaders(custodian, "consumer"), + }); checkResponse(res); } @@ -197,14 +167,10 @@ export function searchPostDocumentReferenceAccessDenied() { "nrl.ods-code": deniedCustodian, "nrl.app-id": "K6PerformanceTest", }); - - const res = http.post( - `https://${__ENV.HOST}/consumer/DocumentReference/_search`, - body, - { - headers: headers, - } - ); + const path = `/DocumentReference/_search`; + const res = http.post(getFullUrl(path, "consumer"), body, { + headers: getHeaders(deniedCustodian, "consumer"), + }); const is_denied = check(res, { "status is 403": (r) => r.status === 403 }); if (!is_denied) { @@ -214,13 +180,10 @@ export function searchPostDocumentReferenceAccessDenied() { export function readDocumentReferenceNotFound() { const { custodian } = getNextPointer(); - - const res = http.get( - `https://${__ENV.HOST}/consumer/DocumentReference/NonExistentID`, - { - headers: getHeaders(custodian), - } - ); + const path = `/DocumentReference/NonExistentID`; + const res = http.post(getFullUrl(path, "consumer"), body, { + headers: getHeaders(custodian, "consumer"), + }); // we expect a 404 here check(res, { "status is 404": (r) => r.status === 404 }); diff --git a/tests/performance/get_test_config.py b/tests/performance/get_test_config.py new file mode 100644 index 000000000..0375548f5 --- /dev/null +++ b/tests/performance/get_test_config.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +import json +import sys + +from botocore.exceptions import ClientError + +from scripts.aws_session_assume import get_account_name, get_boto_session +from tests.utilities.get_access_token import get_bearer_token + + +def get_secret(secret_name: str, session, region: str = "eu-west-2") -> dict: + + client = session.client("secretsmanager", region_name=region) + + try: + response = client.get_secret_value(SecretId=secret_name) + return json.loads(response["SecretString"]) + except ClientError as e: + print(f"Error fetching secret {secret_name}: {e}", file=sys.stderr) # noqa + sys.exit(1) + + +def get_public_mode_config(env_name: str) -> dict: + + try: + boto_session = get_boto_session(env_name) + + params_secret = f"nhsd-nrlf--{env_name}--smoke-test-parameters" + params = get_secret(params_secret, boto_session) + + public_base_url = params.get("public_base_url") + apigee_app_id = params.get("apigee_app_id") + + if not all([public_base_url, apigee_app_id]): + print( # noqa + f"Error: Missing required parameters in {params_secret}", + file=sys.stderr, + ) + print(f"Required: public_base_url, apigee_app_id", file=sys.stderr) # noqa + sys.exit(1) + + account_name = get_account_name(env_name) + bearer_token = get_bearer_token(account_name, apigee_app_id, env_name) + + if not bearer_token: + print(f"Error: Missing required bearer token", file=sys.stderr) # noqa + sys.exit(1) + + config = { + "public_base_url": public_base_url, + "apigee_app_id": apigee_app_id, + "bearer_token": bearer_token, + } + + return config + + except Exception as e: + print(f"Error: {e}", file=sys.stderr) # noqa + sys.exit(1) + + +def main(): + if len(sys.argv) < 2: + print("Usage: get_test_config.py ", file=sys.stderr) # noqa + print("Example: get_test_config.py dev", file=sys.stderr) # noqa + sys.exit(1) + + env_name = sys.argv[1] + + config = get_public_mode_config(env_name) + + print(json.dumps(config)) # noqa + + +if __name__ == "__main__": + main() diff --git a/tests/performance/producer/client_perftest.js b/tests/performance/producer/client_perftest.js index e47f387cb..b649221e7 100644 --- a/tests/performance/producer/client_perftest.js +++ b/tests/performance/producer/client_perftest.js @@ -4,6 +4,7 @@ import { check } from "k6"; import { randomItem } from "https://jslib.k6.io/k6-utils/1.2.0/index.js"; import { crypto } from "k6/experimental/webcrypto"; import { createRecord } from "../setup.js"; +import { getHeaders, getFullUrl } from "../test-config.js"; import exec from "k6/execution"; const distPath = __ENV.DIST_PATH || "./dist"; @@ -38,7 +39,7 @@ function getNextPointer() { const iter = exec.vu.iterationInScenario; const index = iter % dataLines.length; const line = dataLines[index]; - // Adjust field names as per CSV columns: count,pointer_id,pointer_type,custodian,nhs_number + const [pointer_id, pointer_type, custodian, nhs_number] = line .split(",") .map((field) => field.trim()); @@ -115,25 +116,6 @@ pickCustodian = function (typeCode) { return randomItem(arr); }; -function getBaseURL() { - return `https://${__ENV.HOST}/producer/DocumentReference`; -} - -function getHeaders(odsCode = ODS_CODE) { - return { - "Content-Type": "application/fhir+json", - "X-Request-Id": `K6perftest-producer-${exec.scenario.name}-${exec.vu.idInTest}-${exec.vu.iterationInScenario}`, - "NHSD-Correlation-Id": `K6perftest-producer-${exec.scenario.name}-${exec.vu.idInTest}-${exec.vu.iterationInScenario}`, - "NHSD-Connection-Metadata": JSON.stringify({ - "nrl.ods-code": odsCode, - "nrl.app-id": "K6PerformanceTest", - }), - "NHSD-Client-RP-Details": JSON.stringify({ - "developer.app.name": "K6PerformanceTest", - "developer.app.id": "K6PerformanceTest", - }), - }; -} function checkResponse(res) { const is_success = check(res, { "status is 200": (r) => r.status === 200 }); if (!is_success) { @@ -146,9 +128,12 @@ export function createDocumentReference() { const pointerType = pickPointerType(); const custodian = pickCustodian(pointerType); const record = createRecord(nhsNumber, pointerType, custodian); - const res = http.post(getBaseURL(), JSON.stringify(record), { - headers: getHeaders(custodian), + const path = "/DocumentReference"; + + const res = http.post(getFullUrl(path, "producer"), JSON.stringify(record), { + headers: getHeaders(custodian, "producer"), }); + check(res, { "create status is 201": (r) => r.status === 201 }); if (res.status !== 201) { console.warn( @@ -161,9 +146,12 @@ export function createDocumentReference() { export function readDocumentReference() { const { pointer_id, custodian } = getNextPointer(); - const res = http.get(`${getBaseURL()}/${pointer_id}`, { - headers: getHeaders(custodian), + const path = `/DocumentReference/${pointer_id}`; + + const res = http.get(getFullUrl(path, "producer"), { + headers: getHeaders(custodian, "producer"), }); + checkResponse(res); } @@ -172,9 +160,14 @@ export function createThenReadDocumentReference() { const pointerType = pickPointerType(); const custodian = pickCustodian(pointerType); const record = createRecord(nhsNumber, pointerType, custodian); - const createRes = http.post(getBaseURL(), JSON.stringify(record), { - headers: getHeaders(custodian), - }); + const createPath = "/DocumentReference"; + + const createRes = http.post( + getFullUrl(createPath, "producer"), + JSON.stringify(record), + { headers: getHeaders(custodian, "producer") } + ); + check(createRes, { "create status is 201": (r) => r.status === 201 }); if (createRes.status !== 201) { console.warn( @@ -190,9 +183,10 @@ export function createThenReadDocumentReference() { const createdId = locationHeader ? locationHeader.split("/").pop() : record.id; + const path = `/DocumentReference/${createdId}`; - const readRes = http.get(`${getBaseURL()}/${createdId}`, { - headers: getHeaders(custodian), + const readRes = http.get(getFullUrl(path, "producer"), { + headers: getHeaders(custodian, "producer"), }); check(readRes, { "create and read status is 200": (r) => r.status === 200 }); @@ -211,9 +205,14 @@ export function upsertThenReadDocumentReference() { const custodian = pickCustodian(pointerType); const record = createRecord(nhsNumber, pointerType, custodian); record.id = `${custodian}-${crypto.randomUUID()}`; - const upsertRes = http.put(getBaseURL(), JSON.stringify(record), { - headers: getHeaders(custodian), - }); + const upsertPath = "/DocumentReference"; + + const upsertRes = http.put( + getFullUrl(upsertPath, "producer"), + JSON.stringify(record), + { headers: getHeaders(custodian, "producer") } + ); + check(upsertRes, { "upsert status is 201": (r) => r.status === 201 }); if (upsertRes.status !== 201) { console.warn( @@ -225,9 +224,10 @@ export function upsertThenReadDocumentReference() { } const upsertedId = record.id; + const path = `/DocumentReference/${upsertedId}`; - const readRes = http.get(`${getBaseURL()}/${upsertedId}`, { - headers: getHeaders(custodian), + const readRes = http.get(getFullUrl(path, "producer"), { + headers: getHeaders(custodian, "producer"), }); check(readRes, { "upsert and read status is 200": (r) => r.status === 200 }); @@ -245,9 +245,14 @@ export function createThenUpdateDocumentReference() { const pointerType = pickPointerType(); const custodian = pickCustodian(pointerType); const record = createRecord(nhsNumber, pointerType, custodian); - const createRes = http.post(getBaseURL(), JSON.stringify(record), { - headers: getHeaders(custodian), - }); + const path = `/DocumentReference/${upsertedId}`; + + const createRes = http.post( + getFullUrl(path, "producer"), + JSON.stringify(record), + { headers: getHeaders(custodian, "producer") } + ); + check(createRes, { "createThenUpdateDocumentReference: create status is 201": (r) => r.status === 201, @@ -271,13 +276,12 @@ export function createThenUpdateDocumentReference() { // Now update the record record.content[0].attachment.url = "https://example.com/k6-updated-url.pdf"; + const updatePath = `/DocumentReference/${createdId}`; const updateRes = http.put( - `${getBaseURL()}/${createdId}`, + getFullUrl(updatePath, "producer"), JSON.stringify(record), - { - headers: getHeaders(custodian), - } + { headers: getHeaders(custodian, "producer") } ); check(updateRes, { @@ -299,9 +303,14 @@ export function upsertThenUpdateDocumentReference() { const custodian = pickCustodian(pointerType); const record = createRecord(nhsNumber, pointerType, custodian); record.id = `${custodian}-${crypto.randomUUID()}`; - const upsertRes = http.put(getBaseURL(), JSON.stringify(record), { - headers: getHeaders(custodian), - }); + const path = "/DocumentReference"; + + const upsertRes = http.put( + getFullUrl(path, "producer"), + JSON.stringify(record), + { headers: getHeaders(custodian, "producer") } + ); + check(upsertRes, { "upsert status is 201": (r) => r.status === 201 }); if (upsertRes.status !== 201) { console.warn( @@ -317,12 +326,12 @@ export function upsertThenUpdateDocumentReference() { // Now update the record record.content[0].attachment.url = "https://example.com/k6-updated-url.pdf"; + const updatePath = `/DocumentReference/${upsertedId}`; + const updateRes = http.put( - `${getBaseURL()}/${upsertedId}`, + getFullUrl(updatePath, "producer"), JSON.stringify(record), - { - headers: getHeaders(custodian), - } + { headers: getHeaders(custodian, "producer") } ); check(updateRes, { @@ -344,9 +353,12 @@ export function upsertDocumentReference() { const custodian = pickCustodian(pointerType); const record = createRecord(nhsNumber, pointerType, custodian); record.id = `${custodian}-k6perf-${crypto.randomUUID()}`; - const res = http.put(getBaseURL(), JSON.stringify(record), { - headers: getHeaders(custodian), + const path = "/DocumentReference"; + + const res = http.put(getFullUrl(path, "producer"), JSON.stringify(record), { + headers: getHeaders(custodian, "producer"), }); + check(res, { "create status is 201": (r) => r.status === 201 }); if (res.status !== 201) { console.warn( @@ -363,10 +375,12 @@ export function searchDocumentReference() { `https://fhir.nhs.uk/Id/nhs-number|${nhs_number}` ); const type = encodeURIComponent(`http://snomed.info/sct|${pointer_type}`); - const url = `${getBaseURL()}?subject:identifier=${identifier}&type=${type}`; - const res = http.get(url, { - headers: getHeaders(custodian), + const path = `/DocumentReference?subject:identifier=${identifier}&type=${type}`; + + const res = http.get(getFullUrl(path, "producer"), { + headers: getHeaders(custodian, "producer"), }); + check(res, { "searchDocumentReference status is 200": (r) => r.status === 200, }); @@ -383,9 +397,12 @@ export function searchPostDocumentReference() { "subject:identifier": `https://fhir.nhs.uk/Id/nhs-number|${nhs_number}`, type: `http://snomed.info/sct|${pointer_type}`, }); - const res = http.post(`${getBaseURL()}/_search`, body, { - headers: getHeaders(custodian), + const path = `/DocumentReference/_search`; + + const res = http.post(getFullUrl(path, "producer"), body, { + headers: getHeaders(custodian, "producer"), }); + check(res, { "searchPostDocumentReference status is 200": (r) => r.status === 200, }); diff --git a/tests/performance/test-config.js b/tests/performance/test-config.js new file mode 100644 index 000000000..2ff2448c3 --- /dev/null +++ b/tests/performance/test-config.js @@ -0,0 +1,93 @@ +import { POINTER_TYPES } from "./constants.js"; +import exec from "k6/execution"; + +let config = null; + +const connectMode = __ENV.TEST_CONNECT_MODE || "internal"; +let configData = null; + +if (connectMode === "public") { + const configFile = __ENV.TEST_CONFIG_FILE; + if (!configFile) { + throw new Error("Public mode requires TEST_CONFIG_FILE"); + } + configData = JSON.parse(open(configFile)); +} + +function initConfig() { + if (config !== null) { + return config; + } + + if (connectMode === "public") { + config = { + connectMode: "public", + baseUrl: __ENV.TEST_PUBLIC_BASE_URL.replace(/\/$/, ""), + consumerPath: "/consumer/FHIR/R4", + producerPath: "/producer/FHIR/R4", + bearerToken: configData.bearer_token, + }; + + if (!config.baseUrl) { + throw new Error("Public mode requires TEST_PUBLIC_BASE_URL"); + } + if (!config.bearerToken) { + throw new Error("Bearer token not found in config file"); + } + } else { + config = { + connectMode: "internal", + baseUrl: `https://${__ENV.HOST}`, + consumerPath: "/consumer", + producerPath: "/producer", + bearerToken: null, + }; + } + + return config; +} + +export function getHeaders(odsCode, actorType) { + const cfg = initConfig(); + + const baseHeaders = { + "Content-Type": "application/fhir+json", + "X-Request-Id": `K6perftest-${actorType}-${exec.scenario.name}-${exec.vu.idInTest}-${exec.vu.iterationInScenario}`, + "NHSD-Correlation-Id": `K6perftest-${actorType}-${exec.scenario.name}-${exec.vu.idInTest}-${exec.vu.iterationInScenario}`, + }; + + if (cfg.connectMode === "internal") { + return { + ...baseHeaders, + "NHSD-Connection-Metadata": JSON.stringify({ + "nrl.ods-code": odsCode, + "nrl.pointer-types": POINTER_TYPES.map( + (type) => `http://snomed.info/sct|${type}` + ), + "nrl.app-id": "K6PerformanceTest", + }), + "NHSD-Client-RP-Details": JSON.stringify({ + "developer.app.name": "K6PerformanceTest", + "developer.app.id": "K6PerformanceTest", + }), + }; + } else { + return { + ...baseHeaders, + Authorization: `Bearer ${cfg.bearerToken}`, + "NHSD-End-User-Organisation-ODS": odsCode, + }; + } +} + +export function getFullUrl(path, actorType) { + if (!actorType || (actorType !== "consumer" && actorType !== "producer")) { + throw new Error("actorType must be either 'consumer' or 'producer'"); + } + const cfg = initConfig(); + const apiPath = + actorType === "producer" ? cfg.producerPath : cfg.consumerPath; + const fullUrl = `${cfg.baseUrl}${apiPath}${path}`; + + return fullUrl; +} diff --git a/tests/utilities/get_access_token.py b/tests/utilities/get_access_token.py index d76926ebe..fe23b57be 100644 --- a/tests/utilities/get_access_token.py +++ b/tests/utilities/get_access_token.py @@ -109,7 +109,7 @@ def generate_client_assertion(app_secrets: dict): "sub": app_secrets["api_key"], "aud": app_secrets["oauth_url"], "jti": str(uuid4()), - "exp": time() + 300, + "exp": time() + 300, # max:312, still times out at just < 10 mins :( }, app_secrets["private_key"], algorithm="RS512",