diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index b007d1ff3..2e912cc71 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -121,7 +121,7 @@ jobs: REGRESSION_TESTS_PEM: ${{ secrets.REGRESSION_TESTS_PEM }} CLOUD_FORMATION_DEPLOY_ROLE: ${{ secrets.DEV_CLOUD_FORMATION_DEPLOY_ROLE }} TARGET_SPINE_SERVER: ${{ secrets.DEV_TARGET_SPINE_SERVER }} - TARGET_SERVICE_SEARCH_SERVER: ${{ secrets.DEV_TARGET_SERVICE_SEARCH_SERVER }} + TARGET_SERVICE_SEARCH_SERVER: ${{ secrets.DEV_TARGET_SERVICE_SEARCH_v3_SERVER }} release_sandbox_code: needs: [get_issue_number, package_code, get_commit_id] diff --git a/packages/getMyPrescriptions/src/getMyPrescriptions.ts b/packages/getMyPrescriptions/src/getMyPrescriptions.ts index 8ca4c9664..07676f4c5 100644 --- a/packages/getMyPrescriptions/src/getMyPrescriptions.ts +++ b/packages/getMyPrescriptions/src/getMyPrescriptions.ts @@ -24,7 +24,7 @@ import { ResponseFunc } from "./responses" import {extractNHSNumber, NHSNumberValidationError, validateNHSNumber} from "./extractNHSNumber" -import {deepCopy, hasTimedOut, jobWithTimeout} from "./utils" +import {hasTimedOut, jobWithTimeout} from "./utils" import {buildStatusUpdateData, shouldGetStatusUpdates} from "./statusUpdate" import {extractOdsCodes, isolateOperationOutcome} from "./fhirUtils" import {pfpConfig, PfPConfig} from "@pfp-common/utilities" @@ -130,7 +130,7 @@ async function eventHandler( const statusUpdateData = includeStatusUpdateData ? buildStatusUpdateData(logger, searchsetBundle) : undefined const distanceSelling = new DistanceSelling(servicesCache, logger) - const distanceSellingBundle = deepCopy(searchsetBundle) + const distanceSellingBundle = structuredClone(searchsetBundle) const distanceSellingCallout = distanceSelling.search(distanceSellingBundle) const distanceSellingResponse = await jobWithTimeout(params.serviceSearchTimeoutMs, distanceSellingCallout) diff --git a/packages/getMyPrescriptions/src/utils.ts b/packages/getMyPrescriptions/src/utils.ts index a7d5f15a1..326df9b6d 100644 --- a/packages/getMyPrescriptions/src/utils.ts +++ b/packages/getMyPrescriptions/src/utils.ts @@ -1,7 +1,3 @@ -export function deepCopy(obj: T): T { - return JSON.parse(JSON.stringify(obj)) -} - export interface Timeout { isTimeout: true } diff --git a/packages/getMyPrescriptions/tests/statusUpdate.test.ts b/packages/getMyPrescriptions/tests/statusUpdate.test.ts index 9bf72f8b6..1d195a81e 100644 --- a/packages/getMyPrescriptions/tests/statusUpdate.test.ts +++ b/packages/getMyPrescriptions/tests/statusUpdate.test.ts @@ -7,18 +7,29 @@ import { jest } from "@jest/globals" import axios from "axios" +import MockAdapter from "axios-mock-adapter" import {Bundle, MedicationRequest} from "fhir/r4" import {APIGatewayProxyResult as LambdaResult, Context} from "aws-lambda" -import MockAdapter from "axios-mock-adapter" +import {LogLevel} from "@aws-lambda-powertools/logger/types" +import {Logger} from "@aws-lambda-powertools/logger" +import {createSpineClient} from "@NHSDigital/eps-spine-client" +import {MiddyfiedHandler} from "@middy/core" import { + createMockedPfPConfig, helloworldContext, mockAPIResponseBody as mockResponseBody, mockInteractionResponseBody, mockPharmacy2uResponse, mockPharmicaResponse, - mockStateMachineInputEvent + mockStateMachineInputEvent, + MockedPfPConfig, + setupTestEnvironment } from "@pfp-common/testing" +import { + SERVICE_SEARCH_BASE_QUERY_PARAMS, + getServiceSearchEndpoint +} from "@prescriptionsforpatients/serviceSearchClient" import {buildStatusUpdateData} from "../src/statusUpdate" import {StateMachineFunctionResponseBody} from "../src/responses" @@ -29,12 +40,7 @@ import { newHandler, stateMachineEventHandler } from "../src/getMyPrescriptions" -import {EXPECTED_TRACE_IDS, SERVICE_SEARCH_PARAMS} from "./utils" -import {createMockedPfPConfig, MockedPfPConfig, setupTestEnvironment} from "@pfp-common/testing" -import {LogLevel} from "@aws-lambda-powertools/logger/types" -import {Logger} from "@aws-lambda-powertools/logger" -import {createSpineClient} from "@NHSDigital/eps-spine-client" -import {MiddyfiedHandler} from "@middy/core" +import {EXPECTED_TRACE_IDS} from "./utils" const exampleEvent = JSON.stringify(mockStateMachineInputEvent) const exampleInteractionResponse = JSON.stringify(mockInteractionResponseBody) @@ -143,10 +149,10 @@ describe("Unit tests for statusUpdate, via handler", function () { const event: GetMyPrescriptionsEvent = JSON.parse(exampleEvent) mock - .onGet("https://service-search/service-search", {params: {...SERVICE_SEARCH_PARAMS, search: "flm49"}}) + .onGet(getServiceSearchEndpoint(), {params: {...SERVICE_SEARCH_BASE_QUERY_PARAMS, search: "flm49"}}) .reply(200, JSON.parse(pharmacy2uResponse)) mock - .onGet("https://service-search/service-search", {params: {...SERVICE_SEARCH_PARAMS, search: "few08"}}) + .onGet(getServiceSearchEndpoint(), {params: {...SERVICE_SEARCH_BASE_QUERY_PARAMS, search: "few08"}}) .reply(200, JSON.parse(pharmicaResponse)) mock.onGet("https://spine/mm/patientfacingprescriptions").reply(200, JSON.parse(exampleInteractionResponse)) diff --git a/packages/getMyPrescriptions/tests/test-handler.test.ts b/packages/getMyPrescriptions/tests/test-handler.test.ts index dcca05001..8ad14bb6c 100644 --- a/packages/getMyPrescriptions/tests/test-handler.test.ts +++ b/packages/getMyPrescriptions/tests/test-handler.test.ts @@ -1,12 +1,6 @@ import {APIGatewayProxyResult as LambdaResult, Context} from "aws-lambda" -import { - DEFAULT_HANDLER_PARAMS, - newHandler, - GetMyPrescriptionsEvent, - stateMachineEventHandler, - STATE_MACHINE_MIDDLEWARE -} from "../src/getMyPrescriptions" import {Logger} from "@aws-lambda-powertools/logger" +import {LogLevel} from "@aws-lambda-powertools/logger/types" import axios from "axios" import MockAdapter from "axios-mock-adapter" import { @@ -15,23 +9,35 @@ import { it, jest } from "@jest/globals" +import {createSpineClient} from "@NHSDigital/eps-spine-client" +import {MiddyfiedHandler} from "@middy/core" import { + createMockedPfPConfig, mockAPIResponseBody as mockResponseBody, mockInteractionResponseBody, mockPharmacy2uResponse, mockPharmicaResponse, helloworldContext, - mockStateMachineInputEvent + mockStateMachineInputEvent, + MockedPfPConfig, + setupTestEnvironment } from "@pfp-common/testing" +import { + SERVICE_SEARCH_BASE_QUERY_PARAMS, + getServiceSearchEndpoint +} from "@prescriptionsforpatients/serviceSearchClient" +import { + DEFAULT_HANDLER_PARAMS, + newHandler, + GetMyPrescriptionsEvent, + stateMachineEventHandler, + STATE_MACHINE_MIDDLEWARE +} from "../src/getMyPrescriptions" import {HEADERS, StateMachineFunctionResponseBody, TIMEOUT_RESPONSE} from "../src/responses" import "./toMatchJsonLogMessage" import {EXPECTED_TRACE_IDS} from "./utils" -import {LogLevel} from "@aws-lambda-powertools/logger/types" -import {createSpineClient} from "@NHSDigital/eps-spine-client" -import {MiddyfiedHandler} from "@middy/core" -import {createMockedPfPConfig, MockedPfPConfig, setupTestEnvironment} from "@pfp-common/testing" const TC008_NHS_NUMBER = "9992387920" @@ -355,14 +361,6 @@ describe("Unit tests for app handler including service search", function () { let testEnv: ReturnType let mockedConfig: MockedPfPConfig - const queryParams = { - "api-version": 2, - searchFields: "ODSCode", - $filter: "OrganisationTypeId eq 'PHA' and OrganisationSubType eq 'DistanceSelling'", - $select: "URL,OrganisationSubType", - $top: 1 - } - beforeEach(() => { testEnv = setupTestEnvironment() mockedConfig = createMockedPfPConfig([TC008_NHS_NUMBER]) @@ -394,11 +392,11 @@ describe("Unit tests for app handler including service search", function () { const event: GetMyPrescriptionsEvent = JSON.parse(exampleStateMachineEvent) mock - .onGet("https://service-search/service-search", {params: {...queryParams, search: "flm49"}}) + .onGet(getServiceSearchEndpoint(), {params: {...SERVICE_SEARCH_BASE_QUERY_PARAMS, search: "flm49"}}) .reply(200, JSON.parse(pharmacy2uResponse)) mock - .onGet("https://service-search/service-search", {params: {...queryParams, search: "few08"}}) + .onGet(getServiceSearchEndpoint(), {params: {...SERVICE_SEARCH_BASE_QUERY_PARAMS, search: "few08"}}) .reply(200, JSON.parse(pharmicaResponse)) mock.onGet("https://spine/mm/patientfacingprescriptions").reply(200, JSON.parse(exampleInteractionResponse)) @@ -434,11 +432,11 @@ describe("Unit tests for app handler including service search", function () { mock.onGet("https://spine/mm/patientfacingprescriptions").reply(200, interactionResponse) mock - .onGet("https://service-search/service-search", {params: {...queryParams, search: "flm49"}}) + .onGet(getServiceSearchEndpoint(), {params: {...SERVICE_SEARCH_BASE_QUERY_PARAMS, search: "flm49"}}) .reply(200, JSON.parse(pharmacy2uResponse)) mock - .onGet("https://service-search/service-search", {params: {...queryParams, search: "few08"}}) + .onGet(getServiceSearchEndpoint(), {params: {...SERVICE_SEARCH_BASE_QUERY_PARAMS, search: "few08"}}) .reply(200, JSON.parse(pharmicaResponse)) const event: GetMyPrescriptionsEvent = JSON.parse(exampleStateMachineEvent) @@ -465,7 +463,7 @@ describe("Unit tests for app handler including service search", function () { mock.onGet("https://spine/mm/patientfacingprescriptions").reply(200, exampleResponse) // eslint-disable-next-line @typescript-eslint/no-unused-vars - mock.onGet("https://service-search/service-search").reply(function (config) { + mock.onGet(getServiceSearchEndpoint()).reply(function (config) { return new Promise((resolve) => setTimeout(() => resolve([200, {}]), 15_000)) }) @@ -502,14 +500,6 @@ describe("Unit tests for logging functionality", function () { let testEnv: ReturnType let mockedConfig: MockedPfPConfig - const queryParams = { - "api-version": 2, - searchFields: "ODSCode", - $filter: "OrganisationTypeId eq 'PHA' and OrganisationSubType eq 'DistanceSelling'", - $select: "URL,OrganisationSubType", - $top: 1 - } - beforeEach(() => { testEnv = setupTestEnvironment() mockedConfig = createMockedPfPConfig([TC008_NHS_NUMBER]) @@ -590,11 +580,11 @@ describe("Unit tests for logging functionality", function () { mock.onGet("https://spine/mm/patientfacingprescriptions").reply(200, interactionResponse) mock - .onGet("https://service-search/service-search", {params: {...queryParams, search: "flm49"}}) + .onGet(getServiceSearchEndpoint(), {params: {...SERVICE_SEARCH_BASE_QUERY_PARAMS, search: "flm49"}}) .reply(200, JSON.parse(pharmacy2uResponse)) mock - .onGet("https://service-search/service-search", {params: {...queryParams, search: "few08"}}) + .onGet(getServiceSearchEndpoint(), {params: {...SERVICE_SEARCH_BASE_QUERY_PARAMS, search: "few08"}}) .reply(200, JSON.parse(pharmicaResponse)) const event: GetMyPrescriptionsEvent = JSON.parse(exampleStateMachineEvent) diff --git a/packages/getMyPrescriptions/tests/utils.ts b/packages/getMyPrescriptions/tests/utils.ts index 32c784675..61b9ef8cd 100644 --- a/packages/getMyPrescriptions/tests/utils.ts +++ b/packages/getMyPrescriptions/tests/utils.ts @@ -12,14 +12,6 @@ export function mockInternalDependency(modulePath: string, module: object, depen return mockDependency } -export const SERVICE_SEARCH_PARAMS = { - "api-version": 2, - searchFields: "ODSCode", - $filter: "OrganisationTypeId eq 'PHA' and OrganisationSubType eq 'DistanceSelling'", - $select: "URL,OrganisationSubType", - $top: 1 -} - export const EXPECTED_TRACE_IDS: TraceIDs = { "apigw-request-id": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", "nhsd-correlation-id": "test-request-id.test-correlation-id.rrt-5789322914740101037-b-aet2-20145-482635-2", diff --git a/packages/serviceSearchClient/src/index.ts b/packages/serviceSearchClient/src/index.ts index d0875767b..a5e903bd4 100644 --- a/packages/serviceSearchClient/src/index.ts +++ b/packages/serviceSearchClient/src/index.ts @@ -1,3 +1,2 @@ -import {createServiceSearchClient, ServiceSearchClient} from "./serviceSearch-client" - -export {createServiceSearchClient, ServiceSearchClient} +export {createServiceSearchClient, ServiceSearchClient} from "./serviceSearch-client" +export {SERVICE_SEARCH_BASE_QUERY_PARAMS, getServiceSearchEndpoint} from "./live-serviceSearch-client" diff --git a/packages/serviceSearchClient/src/live-serviceSearch-client.ts b/packages/serviceSearchClient/src/live-serviceSearch-client.ts index 536a806a7..0376c6199 100644 --- a/packages/serviceSearchClient/src/live-serviceSearch-client.ts +++ b/packages/serviceSearchClient/src/live-serviceSearch-client.ts @@ -18,19 +18,29 @@ export type ServiceSearchData = { "value": Array } +export const SERVICE_SEARCH_BASE_QUERY_PARAMS = { + "api-version": 3, + "searchFields": "ODSCode", + "$filter": "OrganisationTypeId eq 'PHA' and OrganisationSubType eq 'DistanceSelling'", + "$select": "URL,OrganisationSubType", + "$top": 1 +} as const + +export function getServiceSearchEndpoint(targetServer?: string): string { + const endpoint = targetServer || process.env.TargetServiceSearchServer || "service-search" + const baseUrl = `https://${endpoint}` + if (endpoint.toLowerCase().includes("api.service.nhs.uk")) { + // service search v3 + return `${baseUrl}/service-search-api/` + } + // service search v2 + return `${baseUrl}/service-search` +} + export class LiveServiceSearchClient implements ServiceSearchClient { - private readonly SERVICE_SEARCH_URL_SCHEME = "https" - private readonly SERVICE_SEARCH_ENDPOINT = process.env.TargetServiceSearchServer private readonly axiosInstance: AxiosInstance private readonly logger: Logger - private readonly outboundHeaders: {"Subscription-Key": string | undefined} - private readonly baseQueryParams: { - "api-version": number, - "searchFields": string, - "$filter": string, - "$select": string, - "$top": number - } + private readonly outboundHeaders: {"apikey": string | undefined, "Subscription-Key": string | undefined} constructor(logger: Logger) { this.logger = logger @@ -39,17 +49,17 @@ export class LiveServiceSearchClient implements ServiceSearchClient { axiosRetry(this.axiosInstance, {retries: 3}) this.axiosInstance.interceptors.request.use((config) => { - config.headers["request-startTime"] = new Date().getTime() + config.headers["request-startTime"] = Date.now() return config }) this.axiosInstance.interceptors.response.use((response) => { - const currentTime = new Date().getTime() + const currentTime = Date.now() const startTime = response.config.headers["request-startTime"] this.logger.info("serviceSearch request duration", {serviceSearch_duration: currentTime - startTime}) return response }, (error) => { - const currentTime = new Date().getTime() + const currentTime = Date.now() const startTime = error.config?.headers["request-startTime"] this.logger.info("serviceSearch request duration", {serviceSearch_duration: currentTime - startTime}) @@ -85,21 +95,15 @@ export class LiveServiceSearchClient implements ServiceSearchClient { }) this.outboundHeaders = { - "Subscription-Key": process.env.ServiceSearchApiKey - } - this.baseQueryParams = { - "api-version": 2, - "searchFields": "ODSCode", - "$filter": "OrganisationTypeId eq 'PHA' and OrganisationSubType eq 'DistanceSelling'", - "$select": "URL,OrganisationSubType", - "$top": 1 + "Subscription-Key": process.env.ServiceSearchApiKey, + "apikey": process.env.ServiceSearch3ApiKey } } async searchService(odsCode: string): Promise { try { - const address = this.getServiceSearchEndpoint() - const queryParams = {...this.baseQueryParams, search: odsCode} + const address = getServiceSearchEndpoint() + const queryParams = {...SERVICE_SEARCH_BASE_QUERY_PARAMS, search: odsCode} this.logger.info(`making request to ${address} with ods code ${odsCode}`, {odsCode: odsCode}) const response = await this.axiosInstance.get(address, { @@ -154,16 +158,14 @@ export class LiveServiceSearchClient implements ServiceSearchClient { } stripApiKeyFromHeaders(error: AxiosError) { - const headerKey = "subscription-key" - if (error.response?.headers) { - delete error.response.headers[headerKey] - } - if (error.request?.headers) { - delete error.request.headers[headerKey] - } - } - - private getServiceSearchEndpoint() { - return `${this.SERVICE_SEARCH_URL_SCHEME}://${this.SERVICE_SEARCH_ENDPOINT}/service-search` + const headerKeys = ["subscription-key", "apikey"] + headerKeys.forEach((key) => { + if (error.response?.headers) { + delete error.response.headers[key] + } + if (error.request?.headers) { + delete error.request.headers[key] + } + }) } } diff --git a/packages/serviceSearchClient/tests/live-serviceSearch-client.test.ts b/packages/serviceSearchClient/tests/live-serviceSearch-client.test.ts index cc8219193..39decbfc3 100644 --- a/packages/serviceSearchClient/tests/live-serviceSearch-client.test.ts +++ b/packages/serviceSearchClient/tests/live-serviceSearch-client.test.ts @@ -28,9 +28,10 @@ describe("live serviceSearch client", () => { jest.restoreAllMocks() }) - // Private helper tests - test("getServiceSearchEndpoint returns correct URL", () => { - const endpoint = client["getServiceSearchEndpoint"]() + // Helper function tests + test("getServiceSearchEndpoint returns correct URL", async () => { + const {getServiceSearchEndpoint} = await import("../src/live-serviceSearch-client.js") + const endpoint = getServiceSearchEndpoint() expect(endpoint).toBe(serviceSearchUrl) })