From 7bcd602e45027722c5e690699cc5bdded1f37c79 Mon Sep 17 00:00:00 2001 From: Sandy Forrester Date: Fri, 5 Dec 2025 17:03:51 +0000 Subject: [PATCH 01/16] NRL-1841 Enable public mode for consumer performance tests --- Makefile | 54 ++++++++++++--- tests/performance/consumer/client.js | 100 +++++++++------------------ tests/performance/get_test_config.py | 77 +++++++++++++++++++++ tests/performance/test-config.js | 79 +++++++++++++++++++++ 4 files changed, 235 insertions(+), 75 deletions(-) create mode 100644 tests/performance/get_test_config.py create mode 100644 tests/performance/test-config.js diff --git a/Makefile b/Makefile index 6c754d888..5f33a7338 100644 --- a/Makefile +++ b/Makefile @@ -151,18 +151,56 @@ 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-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..." + $(eval TEST_CONFIG := $(shell PYTHONPATH=. python3 tests/performance/get_test_config.py $(ENV_TYPE) 2>&1 | tail -n 1)) + $(eval PUBLIC_BASE_URL := $(shell echo '$(TEST_CONFIG)' | jq -r '.public_base_url')) + $(eval BEARER_TOKEN := $(shell echo '$(TEST_CONFIG)' | jq -r '.bearer_token')) + @echo "Running consumer performance baseline test against the external access points" + TEST_CONNECT_MODE=public \ + TEST_PUBLIC_BASE_URL=$(PUBLIC_BASE_URL) \ + TEST_BEARER_TOKEN=$(BEARER_TOKEN) \ + 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-stress: - @echo "Running consumer performance stress test" +test-performance-stress-public: + @echo "Fetching public mode configuration and bearer token..." + $(eval TEST_CONFIG := $(shell PYTHONPATH=. python3 tests/performance/get_test_config.py $(ENV_TYPE) 2>&1 | tail -n 1)) + $(eval PUBLIC_BASE_URL := $(shell echo '$(TEST_CONFIG)' | jq -r '.public_base_url')) + $(eval BEARER_TOKEN := $(shell echo '$(TEST_CONFIG)' | jq -r '.bearer_token')) + TEST_CONNECT_MODE=public \ + TEST_PUBLIC_BASE_URL=$(PUBLIC_BASE_URL) \ + TEST_BEARER_TOKEN=$(BEARER_TOKEN) \ + @echo "Running consumer performance stress test against the external access points" ; \ 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-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: + @echo "Fetching public mode configuration and bearer token..." + $(eval TEST_CONFIG := $(shell PYTHONPATH=. python3 tests/performance/get_test_config.py $(ENV_TYPE) 2>&1 | tail -n 1)) + $(eval PUBLIC_BASE_URL := $(shell echo '$(TEST_CONFIG)' | jq -r '.public_base_url')) + $(eval BEARER_TOKEN := $(shell echo '$(TEST_CONFIG)' | jq -r '.bearer_token')) + @echo "heres what PUBLIC_BASE_URL looks like $(PUBLIC_BASE_URL)" + @echo "heres what BEARER_TOKEN looks like $(BEARER_TOKEN)" + @echo "heres what raw TEST_CONFIG looks like $(TEST_CONFIG)" + TEST_CONNECT_MODE=public \ + TEST_PUBLIC_BASE_URL=$(PUBLIC_BASE_URL) \ + TEST_BEARER_TOKEN=$(BEARER_TOKEN) \ + @echo "Running consumer performance soak test against the external access points" ; \ k6 run --out csv=$(DIST_PATH)/consumer-soak.csv tests/performance/consumer/soak.js -e HOST=$(HOST) -e ENV_TYPE=$(ENV_TYPE) test-performance-output: ## Process outputs from the performance tests diff --git a/tests/performance/consumer/client.js b/tests/performance/consumer/client.js index d8bc3cbd2..1991f994c 100644 --- a/tests/performance/consumer/client.js +++ b/tests/performance/consumer/client.js @@ -1,32 +1,13 @@ +import { getHeaders, getFullUrl } from "../test-config.js"; import { - NHS_NUMBERS, - POINTER_IDS, POINTER_TYPES, - ODS_CODE, CATEGORIES, + NHS_NUMBERS, + POINTER_IDS, } 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 +22,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), { + headers: getHeaders(), + }); checkResponse(res); } @@ -54,12 +34,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), { + headers: getHeaders(), + }); checkResponse(res); } @@ -74,12 +52,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), { + headers: getHeaders(), + }); checkResponse(res); } @@ -95,12 +71,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), { + headers: getHeaders(), + }); checkResponse(res); } @@ -114,13 +88,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), body, { + headers: getHeaders(), + }); checkResponse(res); } @@ -133,13 +104,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), body, { + headers: getHeaders(), + }); checkResponse(res); } @@ -150,12 +118,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), body, { + headers: getHeaders(), + }); checkResponse(res); } diff --git a/tests/performance/get_test_config.py b/tests/performance/get_test_config.py new file mode 100644 index 000000000..359bd7fe0 --- /dev/null +++ b/tests/performance/get_test_config.py @@ -0,0 +1,77 @@ +#!/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) + + # TODO: Add secret specific to performance tests + 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/test-config.js b/tests/performance/test-config.js new file mode 100644 index 000000000..532bfef65 --- /dev/null +++ b/tests/performance/test-config.js @@ -0,0 +1,79 @@ +import { POINTER_TYPES, ODS_CODE } from "./constants.js"; + +let config = null; + +function initConfig() { + if (config !== null) { + return config; + } + + const connectMode = __ENV.TEST_CONNECT_MODE || "internal"; + + if (connectMode === "public") { + config = { + connectMode: "public", + baseUrl: __ENV.TEST_PUBLIC_BASE_URL.replace(/\/$/, ""), + consumerPath: "/consumer/FHIR/R4", + producerPath: "/producer/FHIR/R4", + odsCode: ODS_CODE, + bearerToken: __ENV.TEST_BEARER_TOKEN, + }; + + if (!config.baseUrl) { + throw new Error("Public mode requires TEST_PUBLIC_BASE_URL"); + } + if (!config.bearerToken) { + throw new Error("Public mode requires TEST_BEARER_TOKEN"); + } + } else { + config = { + connectMode: "internal", + baseUrl: `https://${__ENV.HOST}`, + consumerPath: "/consumer", + producerPath: "/producer", + odsCode: ODS_CODE, + bearerToken: null, + }; + } + + return config; +} + +export function getHeaders(appId = "K6PerformanceTest") { + const cfg = initConfig(); + + const baseHeaders = { + "Content-Type": "application/fhir+json", + "X-Request-Id": appId, + "NHSD-Correlation-Id": appId, + }; + + if (cfg.connectMode === "internal") { + return { + ...baseHeaders, + "NHSD-Connection-Metadata": JSON.stringify({ + "nrl.ods-code": cfg.odsCode, + "nrl.pointer-types": POINTER_TYPES.map( + (type) => `http://snomed.info/sct|${type}` + ), + "nrl.app-id": appId, + }), + "NHSD-Client-RP-Details": JSON.stringify({ + "developer.app.name": appId, + "developer.app.id": appId, + }), + }; + } else { + return { + ...baseHeaders, + Authorization: `Bearer ${cfg.bearerToken}`, + "NHSD-End-User-Organisation-ODS": cfg.odsCode, + }; + } +} + +export function getFullUrl(path, apiType = "consumer") { + const cfg = initConfig(); + const apiPath = apiType === "producer" ? cfg.producerPath : cfg.consumerPath; + return `${cfg.baseUrl}${apiPath}${path}`; +} From 17c690d65fd6d0ec325cc7cd84680c3e6916456a Mon Sep 17 00:00:00 2001 From: Sandy Forrester Date: Fri, 5 Dec 2025 17:07:32 +0000 Subject: [PATCH 02/16] NRL-1841 Remove comments --- Makefile | 3 --- 1 file changed, 3 deletions(-) diff --git a/Makefile b/Makefile index 5f33a7338..577a03ce7 100644 --- a/Makefile +++ b/Makefile @@ -194,9 +194,6 @@ test-performance-soak-public: $(eval TEST_CONFIG := $(shell PYTHONPATH=. python3 tests/performance/get_test_config.py $(ENV_TYPE) 2>&1 | tail -n 1)) $(eval PUBLIC_BASE_URL := $(shell echo '$(TEST_CONFIG)' | jq -r '.public_base_url')) $(eval BEARER_TOKEN := $(shell echo '$(TEST_CONFIG)' | jq -r '.bearer_token')) - @echo "heres what PUBLIC_BASE_URL looks like $(PUBLIC_BASE_URL)" - @echo "heres what BEARER_TOKEN looks like $(BEARER_TOKEN)" - @echo "heres what raw TEST_CONFIG looks like $(TEST_CONFIG)" TEST_CONNECT_MODE=public \ TEST_PUBLIC_BASE_URL=$(PUBLIC_BASE_URL) \ TEST_BEARER_TOKEN=$(BEARER_TOKEN) \ From a69ad75467a36a4e07106dd9832eaef9fc89a8ae Mon Sep 17 00:00:00 2001 From: Sandy Forrester Date: Mon, 8 Dec 2025 12:06:26 +0000 Subject: [PATCH 03/16] NRL-1841 Store public test config in tmp file --- Makefile | 48 +++++++++++++++++--------------- tests/performance/test-config.js | 17 ++++++++--- 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/Makefile b/Makefile index 577a03ce7..046f008ed 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,3 @@ - .EXPORT_ALL_VARIABLES: .NOTPARALLEL: .PHONY: * @@ -161,44 +160,47 @@ test-performance-baseline-internal: check-warn ## Run the performance baseline t test-performance-baseline-public: check-warn ## Run the baseline performance tests for the external access points @echo "Fetching public mode configuration and bearer token..." - $(eval TEST_CONFIG := $(shell PYTHONPATH=. python3 tests/performance/get_test_config.py $(ENV_TYPE) 2>&1 | tail -n 1)) - $(eval PUBLIC_BASE_URL := $(shell echo '$(TEST_CONFIG)' | jq -r '.public_base_url')) - $(eval BEARER_TOKEN := $(shell echo '$(TEST_CONFIG)' | jq -r '.bearer_token')) - @echo "Running consumer performance baseline test against the external access points" + @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_BEARER_TOKEN=$(BEARER_TOKEN) \ + 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-stress-public: +test-performance-stress-public: check-warn ## Run the stress performance tests for the external access points @echo "Fetching public mode configuration and bearer token..." - $(eval TEST_CONFIG := $(shell PYTHONPATH=. python3 tests/performance/get_test_config.py $(ENV_TYPE) 2>&1 | tail -n 1)) - $(eval PUBLIC_BASE_URL := $(shell echo '$(TEST_CONFIG)' | jq -r '.public_base_url')) - $(eval BEARER_TOKEN := $(shell echo '$(TEST_CONFIG)' | jq -r '.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_BEARER_TOKEN=$(BEARER_TOKEN) \ - @echo "Running consumer performance stress test against the external access points" ; \ - k6 run --out csv=$(DIST_PATH)/consumer-stress.csv tests/performance/consumer/stress.js -e HOST=$(HOST) -e ENV_TYPE=$(ENV_TYPE) + 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: +test-performance-soak-public: check-warn ## Run the soak performance tests for the external access points @echo "Fetching public mode configuration and bearer token..." - $(eval TEST_CONFIG := $(shell PYTHONPATH=. python3 tests/performance/get_test_config.py $(ENV_TYPE) 2>&1 | tail -n 1)) - $(eval PUBLIC_BASE_URL := $(shell echo '$(TEST_CONFIG)' | jq -r '.public_base_url')) - $(eval BEARER_TOKEN := $(shell echo '$(TEST_CONFIG)' | jq -r '.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_BEARER_TOKEN=$(BEARER_TOKEN) \ - @echo "Running consumer performance soak test against the external access points" ; \ - k6 run --out csv=$(DIST_PATH)/consumer-soak.csv tests/performance/consumer/soak.js -e HOST=$(HOST) -e ENV_TYPE=$(ENV_TYPE) + 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" diff --git a/tests/performance/test-config.js b/tests/performance/test-config.js index 532bfef65..640557aec 100644 --- a/tests/performance/test-config.js +++ b/tests/performance/test-config.js @@ -2,13 +2,22 @@ import { POINTER_TYPES, ODS_CODE } from "./constants.js"; 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; } - const connectMode = __ENV.TEST_CONNECT_MODE || "internal"; - if (connectMode === "public") { config = { connectMode: "public", @@ -16,14 +25,14 @@ function initConfig() { consumerPath: "/consumer/FHIR/R4", producerPath: "/producer/FHIR/R4", odsCode: ODS_CODE, - bearerToken: __ENV.TEST_BEARER_TOKEN, + bearerToken: configData.bearer_token, }; if (!config.baseUrl) { throw new Error("Public mode requires TEST_PUBLIC_BASE_URL"); } if (!config.bearerToken) { - throw new Error("Public mode requires TEST_BEARER_TOKEN"); + throw new Error("Bearer token not found in config file"); } } else { config = { From 2d58983370608e787bf8b836dc2c119ed5770819 Mon Sep 17 00:00:00 2001 From: Sandy Forrester Date: Mon, 8 Dec 2025 15:09:27 +0000 Subject: [PATCH 04/16] NRL-1841 Remove default consumer behaviour from getFullUrl --- tests/performance/consumer/client.js | 14 +++++++------- tests/performance/test-config.js | 12 +++++++++--- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/tests/performance/consumer/client.js b/tests/performance/consumer/client.js index 1991f994c..9815ed60b 100644 --- a/tests/performance/consumer/client.js +++ b/tests/performance/consumer/client.js @@ -24,7 +24,7 @@ export function countDocumentReference() { ); const path = `/DocumentReference?_summary=count&subject:identifier=${identifier}`; - const res = http.get(getFullUrl(path), { + const res = http.get(getFullUrl(path, "consumer"), { headers: getHeaders(), }); checkResponse(res); @@ -35,7 +35,7 @@ export function readDocumentReference() { const id = POINTER_IDS[choice]; const path = `/DocumentReference/${id}`; - const res = http.get(getFullUrl(path), { + const res = http.get(getFullUrl(path, "consumer"), { headers: getHeaders(), }); @@ -53,7 +53,7 @@ export function searchDocumentReference() { const type = encodeURIComponent(`http://snomed.info/sct|${pointer_type}`); const path = `/DocumentReference?subject:identifier=${identifier}&type=${type}`; - const res = http.get(getFullUrl(path), { + const res = http.get(getFullUrl(path, "consumer"), { headers: getHeaders(), }); checkResponse(res); @@ -72,7 +72,7 @@ export function searchDocumentReferenceByCategory() { ); const path = `/DocumentReference?subject:identifier=${identifier}&category=${category}`; - const res = http.get(getFullUrl(path), { + const res = http.get(getFullUrl(path, "consumer"), { headers: getHeaders(), }); checkResponse(res); @@ -89,7 +89,7 @@ export function searchPostDocumentReference() { }); const path = `/DocumentReference/_search`; - const res = http.post(getFullUrl(path), body, { + const res = http.post(getFullUrl(path, "consumer"), body, { headers: getHeaders(), }); checkResponse(res); @@ -105,7 +105,7 @@ export function searchPostDocumentReferenceByCategory() { }); const path = `/DocumentReference/_search`; - const res = http.post(getFullUrl(path), body, { + const res = http.post(getFullUrl(path, "consumer"), body, { headers: getHeaders(), }); checkResponse(res); @@ -120,7 +120,7 @@ export function countPostDocumentReference() { }); const path = `/DocumentReference/_search?_summary=count`; - const res = http.post(getFullUrl(path), body, { + const res = http.post(getFullUrl(path, "consumer"), body, { headers: getHeaders(), }); checkResponse(res); diff --git a/tests/performance/test-config.js b/tests/performance/test-config.js index 640557aec..be96c151b 100644 --- a/tests/performance/test-config.js +++ b/tests/performance/test-config.js @@ -81,8 +81,14 @@ export function getHeaders(appId = "K6PerformanceTest") { } } -export function getFullUrl(path, apiType = "consumer") { +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 = apiType === "producer" ? cfg.producerPath : cfg.consumerPath; - return `${cfg.baseUrl}${apiPath}${path}`; + const apiPath = + actorType === "producer" ? cfg.producerPath : cfg.consumerPath; + const fullUrl = `${cfg.baseUrl}${apiPath}${path}`; + + return fullUrl; } From 48e7f01c0b519e31bb73d4fd66bce3e1676e652f Mon Sep 17 00:00:00 2001 From: Sandy Forrester Date: Tue, 6 Jan 2026 10:37:02 +0000 Subject: [PATCH 05/16] NRL-1841 Remove comment --- tests/performance/get_test_config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/performance/get_test_config.py b/tests/performance/get_test_config.py index 359bd7fe0..0375548f5 100644 --- a/tests/performance/get_test_config.py +++ b/tests/performance/get_test_config.py @@ -25,7 +25,6 @@ def get_public_mode_config(env_name: str) -> dict: try: boto_session = get_boto_session(env_name) - # TODO: Add secret specific to performance tests params_secret = f"nhsd-nrlf--{env_name}--smoke-test-parameters" params = get_secret(params_secret, boto_session) From 9a9b2fb88653efe6919b854bc324b1cd8aff02a7 Mon Sep 17 00:00:00 2001 From: Sandy Forrester Date: Tue, 6 Jan 2026 11:13:27 +0000 Subject: [PATCH 06/16] NRL-1841 Update command for running perftests in pipeline --- .github/workflows/pr-env-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-env-deploy.yml b/.github/workflows/pr-env-deploy.yml index 7a0f13128..be6db0348 100644 --- a/.github/workflows/pr-env-deploy.yml +++ b/.github/workflows/pr-env-deploy.yml @@ -300,7 +300,7 @@ 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 From a1756d3ba3fffe83f125baf1a9f7dc4fe3792452 Mon Sep 17 00:00:00 2001 From: Sandy Forrester Date: Tue, 6 Jan 2026 11:42:06 +0000 Subject: [PATCH 07/16] NRL-1841 Update command for running perftests in pipeline --- .github/workflows/pr-env-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-env-deploy.yml b/.github/workflows/pr-env-deploy.yml index be6db0348..76e27080f 100644 --- a/.github/workflows/pr-env-deploy.yml +++ b/.github/workflows/pr-env-deploy.yml @@ -303,7 +303,7 @@ jobs: 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 From 91db15f584c2f4c7d50d27d90100733d75fe4a00 Mon Sep 17 00:00:00 2001 From: Sandy Forrester Date: Wed, 7 Jan 2026 11:54:46 +0000 Subject: [PATCH 08/16] NRL-1841 Integrate public config into perftest tests WIP --- Makefile | 8 ++ tests/performance/consumer/client.js | 15 +- tests/performance/consumer/client_perftest.js | 129 ++++++++---------- tests/performance/test-config.js | 23 ++-- 4 files changed, 82 insertions(+), 93 deletions(-) diff --git a/Makefile b/Makefile index 16b79f8d8..9cc519334 100644 --- a/Makefile +++ b/Makefile @@ -298,6 +298,14 @@ perftest-consumer: @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-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-consumer-public: + @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-prep-generate-producer-data: @echo "Generating producer reference with PERFTEST_TABLE_NAME=$(PERFTEST_TABLE_NAME) and DIST_PATH=$(DIST_PATH)" mkdir -p $(DIST_PATH) diff --git a/tests/performance/consumer/client.js b/tests/performance/consumer/client.js index 9815ed60b..5220a2a74 100644 --- a/tests/performance/consumer/client.js +++ b/tests/performance/consumer/client.js @@ -4,6 +4,7 @@ import { CATEGORIES, NHS_NUMBERS, POINTER_IDS, + ODS_CODE, } from "../constants.js"; import http from "k6/http"; import { check } from "k6"; @@ -25,7 +26,7 @@ export function countDocumentReference() { const path = `/DocumentReference?_summary=count&subject:identifier=${identifier}`; const res = http.get(getFullUrl(path, "consumer"), { - headers: getHeaders(), + headers: getHeaders(ODS_CODE, "consumer"), }); checkResponse(res); } @@ -36,7 +37,7 @@ export function readDocumentReference() { const path = `/DocumentReference/${id}`; const res = http.get(getFullUrl(path, "consumer"), { - headers: getHeaders(), + headers: getHeaders(ODS_CODE, "consumer"), }); checkResponse(res); @@ -54,7 +55,7 @@ export function searchDocumentReference() { const path = `/DocumentReference?subject:identifier=${identifier}&type=${type}`; const res = http.get(getFullUrl(path, "consumer"), { - headers: getHeaders(), + headers: getHeaders(ODS_CODE, "consumer"), }); checkResponse(res); } @@ -73,7 +74,7 @@ export function searchDocumentReferenceByCategory() { const path = `/DocumentReference?subject:identifier=${identifier}&category=${category}`; const res = http.get(getFullUrl(path, "consumer"), { - headers: getHeaders(), + headers: getHeaders(ODS_CODE, "consumer"), }); checkResponse(res); } @@ -90,7 +91,7 @@ export function searchPostDocumentReference() { const path = `/DocumentReference/_search`; const res = http.post(getFullUrl(path, "consumer"), body, { - headers: getHeaders(), + headers: getHeaders(ODS_CODE, "consumer"), }); checkResponse(res); } @@ -106,7 +107,7 @@ export function searchPostDocumentReferenceByCategory() { const path = `/DocumentReference/_search`; const res = http.post(getFullUrl(path, "consumer"), body, { - headers: getHeaders(), + headers: getHeaders(ODS_CODE, "consumer"), }); checkResponse(res); } @@ -121,7 +122,7 @@ export function countPostDocumentReference() { const path = `/DocumentReference/_search?_summary=count`; const res = http.post(getFullUrl(path, "consumer"), body, { - headers: getHeaders(), + headers: getHeaders(ODS_CODE, "consumer"), }); checkResponse(res); } diff --git a/tests/performance/consumer/client_perftest.js b/tests/performance/consumer/client_perftest.js index 25d40b756..76b756d42 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 csvPath = __ENV.DIST_PATH ? `../../../${__ENV.DIST_PATH}/producer_reference_data.csv` @@ -22,21 +23,21 @@ 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 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" @@ -63,26 +64,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); } @@ -95,13 +92,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); } @@ -117,12 +112,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); } @@ -135,13 +128,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); } @@ -155,13 +146,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); } @@ -173,13 +162,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); } @@ -198,14 +184,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) { @@ -215,13 +197,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/test-config.js b/tests/performance/test-config.js index be96c151b..a94948a85 100644 --- a/tests/performance/test-config.js +++ b/tests/performance/test-config.js @@ -1,4 +1,5 @@ -import { POINTER_TYPES, ODS_CODE } from "./constants.js"; +import { POINTER_TYPES } from "./constants.js"; +import exec from "k6/execution"; let config = null; @@ -24,7 +25,7 @@ function initConfig() { baseUrl: __ENV.TEST_PUBLIC_BASE_URL.replace(/\/$/, ""), consumerPath: "/consumer/FHIR/R4", producerPath: "/producer/FHIR/R4", - odsCode: ODS_CODE, + // odsCode: ODS_CODE, bearerToken: configData.bearer_token, }; @@ -40,7 +41,7 @@ function initConfig() { baseUrl: `https://${__ENV.HOST}`, consumerPath: "/consumer", producerPath: "/producer", - odsCode: ODS_CODE, + // odsCode: ODS_CODE, bearerToken: null, }; } @@ -48,35 +49,35 @@ function initConfig() { return config; } -export function getHeaders(appId = "K6PerformanceTest") { +export function getHeaders(odsCode, actorType) { const cfg = initConfig(); const baseHeaders = { "Content-Type": "application/fhir+json", - "X-Request-Id": appId, - "NHSD-Correlation-Id": appId, + "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": cfg.odsCode, + "nrl.ods-code": odsCode, "nrl.pointer-types": POINTER_TYPES.map( (type) => `http://snomed.info/sct|${type}` ), - "nrl.app-id": appId, + "nrl.app-id": "K6PerformanceTest", }), "NHSD-Client-RP-Details": JSON.stringify({ - "developer.app.name": appId, - "developer.app.id": appId, + "developer.app.name": "K6PerformanceTest", + "developer.app.id": "K6PerformanceTest", }), }; } else { return { ...baseHeaders, Authorization: `Bearer ${cfg.bearerToken}`, - "NHSD-End-User-Organisation-ODS": cfg.odsCode, + "NHSD-End-User-Organisation-ODS": odsCode, }; } } From 0776a637676ddf48f1688ad5398c965f505ba7ae Mon Sep 17 00:00:00 2001 From: Sandy Forrester Date: Thu, 8 Jan 2026 16:32:27 +0000 Subject: [PATCH 09/16] NRL-1841 Enable perftest-consumer-public in makefile --- Makefile | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 9cc519334..1d20d77fb 100644 --- a/Makefile +++ b/Makefile @@ -302,9 +302,17 @@ 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-consumer-public: - @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-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=. 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 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)" From 123a1a4840525141d656089a44a6d8dd04a023d2 Mon Sep 17 00:00:00 2001 From: Sandy Forrester Date: Thu, 8 Jan 2026 16:44:26 +0000 Subject: [PATCH 10/16] NRL-1841 Remove commented code --- tests/performance/consumer/client_perftest.js | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tests/performance/consumer/client_perftest.js b/tests/performance/consumer/client_perftest.js index 76b756d42..b86517f43 100644 --- a/tests/performance/consumer/client_perftest.js +++ b/tests/performance/consumer/client_perftest.js @@ -23,22 +23,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]; From cd45774f5becc5201c9f09f0a346d6c3540942a5 Mon Sep 17 00:00:00 2001 From: Sandy Forrester Date: Fri, 9 Jan 2026 15:06:56 +0000 Subject: [PATCH 11/16] NRL-1841 Remove comments and pointer type --- tests/performance/constants.js | 2 +- tests/performance/test-config.js | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) 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/test-config.js b/tests/performance/test-config.js index a94948a85..2ff2448c3 100644 --- a/tests/performance/test-config.js +++ b/tests/performance/test-config.js @@ -25,7 +25,6 @@ function initConfig() { baseUrl: __ENV.TEST_PUBLIC_BASE_URL.replace(/\/$/, ""), consumerPath: "/consumer/FHIR/R4", producerPath: "/producer/FHIR/R4", - // odsCode: ODS_CODE, bearerToken: configData.bearer_token, }; @@ -41,7 +40,6 @@ function initConfig() { baseUrl: `https://${__ENV.HOST}`, consumerPath: "/consumer", producerPath: "/producer", - // odsCode: ODS_CODE, bearerToken: null, }; } From ca1304dad5d2d0ce81fa8d8cf179547208722be3 Mon Sep 17 00:00:00 2001 From: Sandy Forrester Date: Fri, 9 Jan 2026 15:07:54 +0000 Subject: [PATCH 12/16] NRL-1841 Remove redundant makefile command --- Makefile | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Makefile b/Makefile index 1d20d77fb..64dab967e 100644 --- a/Makefile +++ b/Makefile @@ -294,10 +294,6 @@ perftest-producer: @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: - @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-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) From b8a5f0f4951b65f065627bbc75cfecec759467ae Mon Sep 17 00:00:00 2001 From: Sandy Forrester Date: Tue, 13 Jan 2026 17:29:11 +0000 Subject: [PATCH 13/16] NRL-1841 Update Makefile command --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 64dab967e..4ab36fe6a 100644 --- a/Makefile +++ b/Makefile @@ -288,7 +288,7 @@ generate-models: check-warn ## Generate Pydantic Models generate-perftest-permissions: ## Generate perftest permissions and add to nrlf_permissions - poetry run python tests/performance/producer/generate_permissions.py --output_dir="$(DIST_PATH)/nrlf_permissions/K6PerformanceTest" + PYTHONPATH=. poetry run python tests/performance/producer/generate_permissions.py --output_dir="$(DIST_PATH)/nrlf_permissions/K6PerformanceTest" perftest-producer: @echo "Running producer performance tests with HOST=$(PERFTEST_HOST) and ENV_TYPE=$(ENV_TYPE) and DIST_PATH=$(DIST_PATH)" From 964aa45c33eb260cdeab90aa4dcc6ec234021416 Mon Sep 17 00:00:00 2001 From: Anjali Trace Date: Wed, 14 Jan 2026 11:15:34 +0000 Subject: [PATCH 14/16] NRL-1841 Minor fix - run with poetry --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 4ab36fe6a..b87c1d74c 100644 --- a/Makefile +++ b/Makefile @@ -302,7 +302,7 @@ perftest-consumer-public: check-warn ## Run the consumer perftests for the exter @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; \ + 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 \ From cc6938f65926e0d1131d8589bc5fcb13387c1c2e Mon Sep 17 00:00:00 2001 From: Anjali Trace Date: Wed, 14 Jan 2026 14:49:06 +0000 Subject: [PATCH 15/16] NRL-1841 Consumer perf tests working for both internal & public modes --- tests/performance/README.md | 9 ++++++++- tests/performance/consumer/client_perftest.js | 2 +- tests/performance/producer/client_perftest.js | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/performance/README.md b/tests/performance/README.md index 10d3fa5d6..7fc6443a4 100644 --- a/tests/performance/README.md +++ b/tests/performance/README.md @@ -52,7 +52,8 @@ You will need to generate pointer permissions the first time performance tests a ```sh # In project root make 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 @@ -76,6 +77,12 @@ make perftest-consumer ENV_TYPE=perftest PERFTEST_HOST=perftest-1.perftest.recor make perftest-producer ENV_TYPE=perftest PERFTEST_HOST=perftest-1.perftest.record-locator.national.nhs.uk ``` +### Run public tests + +```sh + +``` + ## Assumptions / Caveats - Run performance tests in the perftest environment only\* diff --git a/tests/performance/consumer/client_perftest.js b/tests/performance/consumer/client_perftest.js index b86517f43..c4604566a 100644 --- a/tests/performance/consumer/client_perftest.js +++ b/tests/performance/consumer/client_perftest.js @@ -17,7 +17,7 @@ function getNextPointer() { const iter = exec.vu.iterationInScenario; const index = iter % dataLines.length; const line = dataLines[index]; - const [count, pointer_id, pointer_type, custodian, nhs_number] = line + const [pointer_id, pointer_type, custodian, nhs_number] = line .split(",") .map((field) => field.trim()); return { pointer_id, pointer_type, nhs_number }; diff --git a/tests/performance/producer/client_perftest.js b/tests/performance/producer/client_perftest.js index 5d1cf08c9..0919078bb 100644 --- a/tests/performance/producer/client_perftest.js +++ b/tests/performance/producer/client_perftest.js @@ -40,7 +40,7 @@ function getNextPointer() { 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 [count, pointer_id, pointer_type, custodian, nhs_number] = line + const [pointer_id, pointer_type, custodian, nhs_number] = line .split(",") .map((field) => field.trim()); return { pointer_id, pointer_type, custodian, nhs_number }; From 15f102ea3edded40efff8a967d56cc82ff11296a Mon Sep 17 00:00:00 2001 From: Anjali Trace Date: Thu, 15 Jan 2026 15:27:54 +0000 Subject: [PATCH 16/16] NRL-1841 Producer perf tests run in public mode, same as the consumer ones --- Makefile | 14 +- tests/performance/README.md | 15 ++- tests/performance/producer/client_perftest.js | 127 ++++++++++-------- tests/utilities/get_access_token.py | 2 +- 4 files changed, 99 insertions(+), 59 deletions(-) diff --git a/Makefile b/Makefile index 461064d24..39e7c07fe 100644 --- a/Makefile +++ b/Makefile @@ -311,10 +311,22 @@ 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-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) diff --git a/tests/performance/README.md b/tests/performance/README.md index c2df33894..42a90e0cb 100644 --- a/tests/performance/README.md +++ b/tests/performance/README.md @@ -72,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/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/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",