Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
4 changes: 2 additions & 2 deletions packages/getMyPrescriptions/src/getMyPrescriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 0 additions & 4 deletions packages/getMyPrescriptions/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
export function deepCopy<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj))
}

export interface Timeout {
isTimeout: true
}
Expand Down
26 changes: 16 additions & 10 deletions packages/getMyPrescriptions/tests/statusUpdate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand Down Expand Up @@ -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))
Expand Down
60 changes: 25 additions & 35 deletions packages/getMyPrescriptions/tests/test-handler.test.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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"

Expand Down Expand Up @@ -355,14 +361,6 @@ describe("Unit tests for app handler including service search", function () {
let testEnv: ReturnType<typeof setupTestEnvironment>
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])
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand All @@ -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))
})

Expand Down Expand Up @@ -502,14 +500,6 @@ describe("Unit tests for logging functionality", function () {
let testEnv: ReturnType<typeof setupTestEnvironment>
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])
Expand Down Expand Up @@ -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)
Expand Down
8 changes: 0 additions & 8 deletions packages/getMyPrescriptions/tests/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 2 additions & 3 deletions packages/serviceSearchClient/src/index.ts
Original file line number Diff line number Diff line change
@@ -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"
70 changes: 36 additions & 34 deletions packages/serviceSearchClient/src/live-serviceSearch-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,29 @@ export type ServiceSearchData = {
"value": Array<Service>
}

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
Expand All @@ -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})

Expand Down Expand Up @@ -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<URL | undefined> {
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, {
Expand Down Expand Up @@ -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]
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})

Expand Down
Loading