Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ad26092
CCM-15259 - Improve client subscription management script
rhyscoxnhs Mar 16, 2026
adea584
lint fixes
mjewildnhs Mar 17, 2026
5ff8589
Resolve duplicated code in cli error wrapper
mjewildnhs Mar 17, 2026
a1d65aa
fixup! CCM-15259 - Improve client subscription management script
mjewildnhs Mar 18, 2026
600690b
Resolve duplicated code in cli args, remove project arg
mjewildnhs Mar 18, 2026
68438a8
mutually exclusive json/file arg validation handling
mjewildnhs Mar 18, 2026
e2e3529
Refactor create repository code to reduce duplication
mjewildnhs Mar 18, 2026
3dc9dcb
fixup! Resolve duplicated code in cli args, remove project arg
mjewildnhs Mar 18, 2026
9879c0d
fixup! Refactor create repository code to reduce duplication
mjewildnhs Mar 18, 2026
ab4d21d
Simplify dry run in client put CLI command
mjewildnhs Mar 18, 2026
30be86a
Use statement_id_prefix to fix terraform deployment issues with mock …
mjewildnhs Mar 19, 2026
170cb6c
Refactor config validation to shared package for re-use
mjewildnhs Mar 19, 2026
3b380d2
Use shared config validator
mjewildnhs Mar 19, 2026
d9f38e2
Feedback: refactor config-cache test
mjewildnhs Mar 19, 2026
3cc76f4
Ensure correlationId is used in filtering observability
mjewildnhs Mar 19, 2026
14a1119
Feedback: validate target not used in subscription when deleting it
mjewildnhs Mar 19, 2026
276236c
Feedback: better type safety in setSubscriptionStates
mjewildnhs Mar 19, 2026
09c2075
Fix client subscription fixture in int test
mjewildnhs Mar 19, 2026
4ed3d28
Set isolatedModules to speed up test startup/transpile time
mjewildnhs Mar 19, 2026
e9e2d4b
Fix env var issue in helper test
mjewildnhs Mar 19, 2026
46971df
Silent mode on tests to suppress logging noise
mjewildnhs Mar 19, 2026
76d2eff
Refactor test fixture creation to reduce repeat
mjewildnhs Mar 19, 2026
24f1357
Refactor lambda test fixture creation to reduce repeat
mjewildnhs Mar 19, 2026
6859085
fixup: Refactor lambda test fixture creation to reduce repeat
mjewildnhs Mar 19, 2026
6546351
Silence pinno/logger tests
mjewildnhs Mar 19, 2026
5d937da
Fix target id filter logging
mjewildnhs Mar 19, 2026
1097f8d
fixup! Refactor config validation to shared package for re-use
mjewildnhs Mar 19, 2026
145497a
Refactor cli to have common entry point
mjewildnhs Mar 19, 2026
7cdedd2
Refactor: Split out helper functions
mjewildnhs Mar 20, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -95,17 +95,17 @@ resource "aws_lambda_function_url" "mock_webhook" {

resource "aws_lambda_permission" "mock_webhook_function_url" {
count = var.deploy_mock_webhook ? 1 : 0
statement_id = "FunctionURLAllowPublicAccess"
statement_id_prefix = "FunctionURLAllowPublicAccess"
action = "lambda:InvokeFunctionUrl"
function_name = module.mock_webhook_lambda[0].function_name
principal = "*"
function_url_auth_type = "NONE"
}

resource "aws_lambda_permission" "mock_webhook_function_invoke" {
count = var.deploy_mock_webhook ? 1 : 0
statement_id = "FunctionURLAllowInvokeAction"
action = "lambda:InvokeFunction"
function_name = module.mock_webhook_lambda[0].function_name
principal = "*"
count = var.deploy_mock_webhook ? 1 : 0
statement_id_prefix = "FunctionURLAllowInvokeAction"
action = "lambda:InvokeFunction"
function_name = module.mock_webhook_lambda[0].function_name
principal = "*"
}
1 change: 1 addition & 0 deletions jest.config.base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Config } from "jest";
export const baseJestConfig: Config = {
preset: "ts-jest",
clearMocks: true,
silent: true,
collectCoverage: true,
coverageDirectory: "./.reports/unit/coverage",
coverageProvider: "v8",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import type {
CallbackTarget,
Channel,
ChannelStatus,
ChannelStatusSubscriptionConfiguration,
ClientSubscriptionConfiguration,
MessageStatus,
MessageStatusSubscriptionConfiguration,
SupplierStatus,
} from "@nhs-notify-client-callbacks/models";

export const DEFAULT_TARGET_ID = "00000000-0000-4000-8000-000000000001";

type TargetOverrides = Partial<CallbackTarget> & {
apiKey?: Partial<CallbackTarget["apiKey"]>;
};

export const createTarget = (
overrides: TargetOverrides = {},
): CallbackTarget => ({
targetId: DEFAULT_TARGET_ID,
type: "API",
invocationEndpoint: "https://example.com",
invocationMethod: "POST",
invocationRateLimit: 10,
apiKey: {
headerName: "x-api-key",
headerValue: "secret",
...overrides.apiKey,
},
...overrides,
});

export const createMessageStatusSubscription = (
statuses: MessageStatus[] = ["DELIVERED"],
overrides: Partial<MessageStatusSubscriptionConfiguration> = {},
): MessageStatusSubscriptionConfiguration => ({
subscriptionId: "00000000-0000-0000-0000-000000000001",
subscriptionType: "MessageStatus",
messageStatuses: statuses,
targetIds: [DEFAULT_TARGET_ID],
...overrides,
});

export const createChannelStatusSubscription = (
channelStatuses: ChannelStatus[] = ["DELIVERED"],
supplierStatuses: SupplierStatus[] = ["delivered"],
channelType: Channel = "EMAIL",
overrides: Partial<ChannelStatusSubscriptionConfiguration> = {},
): ChannelStatusSubscriptionConfiguration => ({
subscriptionId: "00000000-0000-0000-0000-000000000002",
subscriptionType: "ChannelStatus",
channelType,
channelStatuses,
supplierStatuses,
targetIds: [DEFAULT_TARGET_ID],
...overrides,
});

export const createClientSubscriptionConfig = (
clientId = "client-1",
overrides: Partial<ClientSubscriptionConfiguration> = {},
): ClientSubscriptionConfiguration => ({
clientId,
subscriptions: [],
targets: [],
...overrides,
});

export const createMessageStatusConfig = (
statuses: MessageStatus[] = ["DELIVERED"],
clientId = "client-1",
): ClientSubscriptionConfiguration =>
createClientSubscriptionConfig(clientId, {
subscriptions: [createMessageStatusSubscription(statuses)],
targets: [createTarget()],
});

export const createChannelStatusConfig = (
channelStatuses: ChannelStatus[] = ["DELIVERED"],
supplierStatuses: SupplierStatus[] = ["delivered"],
clientId = "client-1",
channelType: Channel = "EMAIL",
): ClientSubscriptionConfiguration =>
createClientSubscriptionConfig(clientId, {
subscriptions: [
createChannelStatusSubscription(
channelStatuses,
supplierStatuses,
channelType,
),
],
targets: [createTarget()],
});
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import { GetObjectCommand, NoSuchKey } from "@aws-sdk/client-s3";
import { GetParameterCommand } from "@aws-sdk/client-ssm";
import type { SQSRecord } from "aws-lambda";
import { EventTypes } from "@nhs-notify-client-callbacks/models";
import { createMessageStatusConfig } from "__tests__/helpers/client-subscription-fixtures";
import { createS3Client } from "services/config-loader-service";
import { applicationsMapService, configLoaderService, handler } from "..";

Expand All @@ -73,27 +74,8 @@ const makeSqsRecord = (body: object): SQSRecord => ({
awsRegion: "eu-west-2",
});

const createValidConfig = (clientId: string) => [
{
SubscriptionId: "00000000-0000-0000-0000-000000000001",
ClientId: clientId,
Targets: [
{
Type: "API",
TargetId: "00000000-0000-4000-8000-000000000001",
InvocationEndpoint: "https://example.com/webhook",
InvocationMethod: "POST",
InvocationRateLimit: 10,
APIKey: {
HeaderName: "x-api-key",
HeaderValue: "secret",
},
},
],
SubscriptionType: "MessageStatus",
MessageStatuses: ["DELIVERED", "FAILED"],
},
];
const createValidConfig = (clientId: string) =>
createMessageStatusConfig(["DELIVERED", "FAILED"], clientId);

const validMessageStatusEvent = (clientId: string, messageStatus: string) => ({
specversion: "1.0",
Expand Down
145 changes: 96 additions & 49 deletions lambdas/client-transform-filter-lambda/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,63 +13,57 @@ import type { ConfigLoader } from "services/config-loader";
import type { ApplicationsMapService } from "services/ssm-applications-map";
import { ObservabilityService } from "services/observability";
import { ConfigLoaderService } from "services/config-loader-service";
import {
DEFAULT_TARGET_ID,
createChannelStatusSubscription,
createClientSubscriptionConfig,
createMessageStatusSubscription,
createTarget,
} from "__tests__/helpers/client-subscription-fixtures";
import { createHandler } from "..";

jest.mock("aws-embedded-metrics");

const stubTarget = {
Type: "API",
TargetId: "00000000-0000-4000-8000-000000000001",
InvocationEndpoint: "https://example.com/webhook",
InvocationMethod: "POST",
InvocationRateLimit: 10,
APIKey: { HeaderName: "x-api-key", HeaderValue: "test-api-key" },
};

const createPassthroughConfigLoader = (): ConfigLoader =>
({
loadClientConfig: jest.fn().mockImplementation(async (clientId: string) => [
{
SubscriptionType: "MessageStatus",
SubscriptionId: "00000000-0000-0000-0000-000000000001",
ClientId: clientId,
Targets: [stubTarget],
MessageStatuses: [
"DELIVERED",
"FAILED",
"PENDING",
"SENDING",
"TECHNICAL_FAILURE",
"PERMANENT_FAILURE",
],
},
{
SubscriptionType: "ChannelStatus",
SubscriptionId: "00000000-0000-0000-0000-000000000002",
ClientId: clientId,
Targets: [stubTarget],
ChannelType: "NHSAPP",
ChannelStatuses: ["DELIVERED", "FAILED", "TECHNICAL_FAILURE"],
SupplierStatuses: [
"delivered",
"permanent_failure",
"temporary_failure",
loadClientConfig: jest
.fn()
.mockImplementation(async (clientId: string) => ({
...createClientSubscriptionConfig(clientId),
subscriptions: [
createMessageStatusSubscription(["DELIVERED"], {
subscriptionId: "00000000-0000-0000-0000-000000000001",
targetIds: [DEFAULT_TARGET_ID],
}),
createChannelStatusSubscription(
["DELIVERED"],
["delivered"],
"NHSAPP",
{
subscriptionId: "00000000-0000-0000-0000-000000000002",
targetIds: [DEFAULT_TARGET_ID],
},
),
createChannelStatusSubscription(
["FAILED"],
["permanent_failure"],
"SMS",
{
subscriptionId: "00000000-0000-0000-0000-000000000003",
targetIds: [DEFAULT_TARGET_ID],
},
),
],
},
{
SubscriptionType: "ChannelStatus",
SubscriptionId: "00000000-0000-0000-0000-000000000003",
ClientId: clientId,
Targets: [stubTarget],
ChannelType: "SMS",
ChannelStatuses: ["DELIVERED", "FAILED", "TECHNICAL_FAILURE"],
SupplierStatuses: [
"delivered",
"permanent_failure",
"temporary_failure",
targets: [
createTarget({
invocationEndpoint: "https://example.com/webhook",
apiKey: {
headerName: "x-api-key",
headerValue: "test-api-key",
},
}),
],
},
]),
})),
}) as unknown as ConfigLoader;

const makeStubConfigLoaderService = (): ConfigLoaderService => {
Expand Down Expand Up @@ -185,6 +179,59 @@ describe("Lambda handler", () => {
);
});

it("should record filtering-matched with matched subscription target IDs only", async () => {
const customConfigLoader = {
loadClientConfig: jest.fn().mockResolvedValue(
createClientSubscriptionConfig("client-abc-123", {
subscriptions: [
createMessageStatusSubscription(["DELIVERED"], {
targetIds: ["target-match"],
}),
],
targets: [
createTarget({ targetId: "target-match" }),
createTarget({ targetId: "target-other" }),
],
}),
),
} as unknown as ConfigLoader;

const handlerWithMultipleTargets = createHandler({
createObservabilityService: () =>
new ObservabilityService(mockLogger, mockMetrics, mockMetricsLogger),
createConfigLoaderService: () =>
({ getLoader: () => customConfigLoader }) as ConfigLoaderService,
createApplicationsMapService: makeStubApplicationsMapService,
});

const sqsMessage: SQSRecord = {
messageId: "sqs-msg-id-targets",
receiptHandle: "receipt-handle-targets",
body: JSON.stringify(validMessageStatusEvent),
attributes: {
ApproximateReceiveCount: "1",
SentTimestamp: "1519211230",
SenderId: "ABCDEFGHIJ",
ApproximateFirstReceiveTimestamp: "1519211230",
},
messageAttributes: {},
md5OfBody: "mock-md5",
eventSource: "aws:sqs",
eventSourceARN: "arn:aws:sqs:eu-west-2:123456789:mock-queue",
awsRegion: "eu-west-2",
};

await handlerWithMultipleTargets([sqsMessage]);

expect(mockLogger.info).toHaveBeenCalledWith(
"Callback lifecycle: filtering-matched",
expect.objectContaining({
subscriptionType: "MessageStatus",
targetIds: ["target-match"],
}),
);
});

it("should handle batch of SQS messages from EventBridge Pipes", async () => {
const sqsMessages: SQSRecord[] = [
{
Expand Down
Loading
Loading