Skip to content

Commit aafff52

Browse files
CCM-12858: WIP fixes
1 parent d37a0b7 commit aafff52

File tree

10 files changed

+180
-43
lines changed

10 files changed

+180
-43
lines changed

infrastructure/terraform/components/dl/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ No requirements.
5050
| <a name="module_s3bucket_letters"></a> [s3bucket\_letters](#module\_s3bucket\_letters) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-s3bucket.zip | n/a |
5151
| <a name="module_s3bucket_static_assets"></a> [s3bucket\_static\_assets](#module\_s3bucket\_static\_assets) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-s3bucket.zip | n/a |
5252
| <a name="module_sqs_core_notifier"></a> [sqs\_core\_notifier](#module\_sqs\_core\_notifier) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-sqs.zip | n/a |
53+
| <a name="module_sqs_core_notifier_errors"></a> [sqs\_core\_notifier\_errors](#module\_sqs\_core\_notifier\_errors) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-sqs.zip | n/a |
5354
| <a name="module_sqs_event_publisher_errors"></a> [sqs\_event\_publisher\_errors](#module\_sqs\_event\_publisher\_errors) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-sqs.zip | n/a |
5455
| <a name="module_sqs_pdm_uploader"></a> [sqs\_pdm\_uploader](#module\_sqs\_pdm\_uploader) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-sqs.zip | n/a |
5556
| <a name="module_sqs_ttl"></a> [sqs\_ttl](#module\_sqs\_ttl) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-sqs.zip | n/a |
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
resource "aws_lambda_event_source_mapping" "core_notifier_lambda" {
2+
event_source_arn = module.sqs_core_notifier.sqs_queue_arn
3+
function_name = module.core_notifier.function_arn
4+
batch_size = var.queue_batch_size
5+
maximum_batching_window_in_seconds = var.queue_batch_window_seconds
6+
7+
function_response_types = [
8+
"ReportBatchItemFailures"
9+
]
10+
}

infrastructure/terraform/components/dl/module_lambda_core_notifier.tf

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ module "core_notifier" {
4040
"APIM_ACCESS_TOKEN_SSM_PARAMETER_NAME" = local.apim_access_token_ssm_parameter_name
4141
"EVENT_PUBLISHER_EVENT_BUS_ARN" = aws_cloudwatch_event_bus.main.arn
4242
"EVENT_PUBLISHER_DLQ_URL" = module.sqs_event_publisher_errors.sqs_queue_url
43+
"ENVIRONMENT" = var.environment
4344
}
4445
}
4546

@@ -103,7 +104,7 @@ data "aws_iam_policy_document" "core_notifier_lambda" {
103104
}
104105

105106
statement {
106-
sid = "SQSPermissionsEventPublisherDLQ"
107+
sid = "SQSPermissionsDLQ"
107108
effect = "Allow"
108109

109110
actions = [
@@ -112,6 +113,7 @@ data "aws_iam_policy_document" "core_notifier_lambda" {
112113
]
113114

114115
resources = [
116+
module.sqs_core_notifier_errors.sqs_queue_arn,
115117
module.sqs_event_publisher_errors.sqs_queue_arn,
116118
]
117119
}

infrastructure/terraform/components/dl/module_sqs_core_notifier.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ module "sqs_core_notifier" {
66
environment = var.environment
77
project = var.project
88
region = var.region
9-
name = "core-notifier"
9+
name = "core"
1010

1111
sqs_kms_key_arn = module.kms.key_arn
1212

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module "sqs_core_notifier_errors" {
2+
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-sqs.zip"
3+
4+
aws_account_id = var.aws_account_id
5+
component = local.component
6+
environment = var.environment
7+
project = var.project
8+
region = var.region
9+
name = "core_notifier-errors"
10+
11+
sqs_kms_key_arn = module.kms.key_arn
12+
13+
visibility_timeout_seconds = 60
14+
}

lambdas/core-notifier-lambda/src/__tests__/app/parse-sqs-message.test.ts

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,6 @@ import { parseSqsRecord } from 'app/parse-sqs-message';
55
import { InvalidPdmResourceAvailableEvent } from 'domain/invalid-pdm-resource-available-event';
66
import { validPdmEvent } from '__tests__/constants';
77

8-
// Import the mocked validator after the mock setup
9-
import { messageDownloadedValidator } from 'digital-letters-events/PDMResourceAvailable.js';
10-
11-
jest.mock('digital-letters-events/PDMResourceAvailable.js', () => ({
12-
messageDownloadedValidator: jest.fn(),
13-
}));
14-
158
const mockLogger = mock<Logger>();
169

1710
describe('parseSqsRecord', () => {
@@ -20,9 +13,7 @@ describe('parseSqsRecord', () => {
2013
const createSqsRecord = (detail: any): SQSRecord => ({
2114
messageId,
2215
receiptHandle: 'receipt-handle',
23-
body: JSON.stringify({
24-
detail,
25-
}),
16+
body: JSON.stringify(detail),
2617
attributes: {
2718
ApproximateReceiveCount: '1',
2819
SentTimestamp: '1234567890',
@@ -43,50 +34,43 @@ describe('parseSqsRecord', () => {
4334
describe('when SQS record contains a valid PDMResourceAvailable event', () => {
4435
it('parses and returns the PDMResourceAvailable event', () => {
4536
const sqsRecord = createSqsRecord(validPdmEvent);
46-
(messageDownloadedValidator as jest.Mock).mockReturnValueOnce(true);
4737

4838
const result = parseSqsRecord(sqsRecord, mockLogger);
4939

5040
expect(result).toEqual(validPdmEvent);
5141
expect(mockLogger.info).toHaveBeenCalledWith('Parsing SQS Record', {
5242
messageId,
43+
body: sqsRecord.body,
5344
});
5445
expect(mockLogger.info).toHaveBeenCalledWith(
5546
'Parsed valid PDMResourceAvailable Event',
5647
{
5748
messageId,
49+
messageReference: validPdmEvent.data.messageReference,
50+
senderId: validPdmEvent.data.senderId,
51+
resourceId: validPdmEvent.data.resourceId,
5852
},
5953
);
60-
expect(messageDownloadedValidator).toHaveBeenCalledWith(validPdmEvent);
6154
});
6255
});
6356

6457
describe('when SQS record contains an invalid PDMResourceAvailable event', () => {
6558
it('logs error and throws InvalidPdmResourceAvailableEvent', () => {
6659
const invalidEvent = { ...validPdmEvent, data: {} };
6760
const sqsRecord = createSqsRecord(invalidEvent);
68-
const validationErrors = [
69-
{
70-
instancePath: '/data',
71-
schemaPath: '#/properties/data/required',
72-
keyword: 'required',
73-
params: { missingProperty: 'senderId' },
74-
message: "must have required property 'senderId'",
75-
},
76-
];
77-
(messageDownloadedValidator as jest.Mock).mockReturnValueOnce(false);
78-
messageDownloadedValidator.errors = validationErrors;
7961

8062
expect(() => parseSqsRecord(sqsRecord, mockLogger)).toThrow(
8163
InvalidPdmResourceAvailableEvent,
8264
);
8365

84-
expect(mockLogger.error).toHaveBeenCalledWith({
85-
err: validationErrors,
86-
description:
87-
'The SQS message does not contain a valid PDMResourceAvailable event',
88-
messageId,
89-
});
66+
expect(mockLogger.error).toHaveBeenCalledWith(
67+
expect.objectContaining({
68+
description:
69+
'The SQS message does not contain a valid PDMResourceAvailable event',
70+
messageId,
71+
err: expect.any(Array),
72+
}),
73+
);
9074
});
9175
});
9276

@@ -112,6 +96,7 @@ describe('parseSqsRecord', () => {
11296
expect(() => parseSqsRecord(sqsRecord, mockLogger)).toThrow(SyntaxError);
11397
expect(mockLogger.info).toHaveBeenCalledWith('Parsing SQS Record', {
11498
messageId,
99+
body: sqsRecord.body,
115100
});
116101
});
117102
});

lambdas/core-notifier-lambda/src/__tests__/constants.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,19 +60,20 @@ export const mockResponse: SingleMessageResponse = {
6060

6161
export const validPdmEvent: PDMResourceAvailable = {
6262
id: 'event-id-123',
63-
source: 'urn:nhs:names:services:notify:pdm',
63+
source:
64+
'/nhs/england/notify/development/dev-12345/data-plane/digitalletters/pdm',
6465
specversion: '1.0',
6566
type: 'uk.nhs.notify.digital.letters.pdm.resource.available.v1',
6667
time: '2025-12-15T10:00:00Z',
6768
datacontenttype: 'application/json',
6869
subject: 'message-subject-123',
69-
traceparent: '00-trace-parent',
70+
traceparent: '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
7071
recordedtime: '2025-12-15T10:00:00Z',
7172
severitynumber: 2,
7273
data: {
7374
senderId: 'sender-123',
7475
messageReference: 'msg-ref-123',
75-
resourceId: 'ResourceId-123',
76+
resourceId: 'f5524783-e5d7-473e-b2a0-29582ff231da',
7677
nhsNumber: '9991234566',
7778
odsCode: 'A12345',
7879
},

lambdas/core-notifier-lambda/src/app/parse-sqs-message.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,22 @@ import { SQSRecord } from 'aws-lambda';
22
import { Logger } from 'utils';
33
import { PDMResourceAvailable } from 'digital-letters-events';
44
import { InvalidPdmResourceAvailableEvent } from 'domain/invalid-pdm-resource-available-event';
5-
import { messageDownloadedValidator } from 'digital-letters-events/PDMResourceAvailable.js';
5+
import messagePDMResourceAvailableValidator from 'digital-letters-events/PDMResourceAvailable.js';
66

77
export const parseSqsRecord = (
88
sqsRecord: SQSRecord,
99
logger: Logger,
1010
): PDMResourceAvailable => {
1111
logger.info('Parsing SQS Record', {
1212
messageId: sqsRecord.messageId,
13+
body: sqsRecord.body,
1314
});
15+
1416
const sqsEventBody = JSON.parse(sqsRecord.body);
15-
const sqsEventDetail = sqsEventBody.detail;
16-
const isEventValid = messageDownloadedValidator(sqsEventDetail);
17-
if (!isEventValid) {
17+
18+
if (!messagePDMResourceAvailableValidator(sqsEventBody)) {
1819
logger.error({
19-
err: messageDownloadedValidator.errors,
20+
err: messagePDMResourceAvailableValidator.errors,
2021
description:
2122
'The SQS message does not contain a valid PDMResourceAvailable event',
2223
messageId: sqsRecord.messageId,
@@ -26,7 +27,10 @@ export const parseSqsRecord = (
2627

2728
logger.info('Parsed valid PDMResourceAvailable Event', {
2829
messageId: sqsRecord.messageId,
30+
messageReference: sqsEventBody.data.messageReference,
31+
senderId: sqsEventBody.data.senderId,
32+
resourceId: sqsEventBody.data.resourceId,
2933
});
3034

31-
return sqsEventDetail as PDMResourceAvailable;
35+
return sqsEventBody;
3236
};

lambdas/core-notifier-lambda/src/infra/event-publisher-facade.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@ import messageRequestSubmittedValidator from 'digital-letters-events/MessageRequ
88
import messageRequestRejectedValidator from 'digital-letters-events/MessageRequestRejected.js';
99
import messageRequestSkippedValidator from 'digital-letters-events/MessageRequestSkipped.js';
1010

11+
const submittedEventValidator = messageRequestSubmittedValidator as (
12+
d: unknown,
13+
) => d is MessageRequestSubmitted;
14+
15+
const rejectedEventValidator = messageRequestRejectedValidator as (
16+
d: unknown,
17+
) => d is MessageRequestRejected;
18+
19+
const skippedEventValidator = messageRequestSkippedValidator as (
20+
d: unknown,
21+
) => d is MessageRequestSkipped;
22+
1123
export class EventPublisherFacade {
1224
constructor(
1325
private readonly messageRequestSubmittedEventPublisher: EventPublisher,
@@ -24,7 +36,7 @@ export class EventPublisherFacade {
2436
);
2537
this.messageRequestSubmittedEventPublisher.sendEvents<MessageRequestSubmitted>(
2638
[event],
27-
messageRequestSubmittedValidator,
39+
submittedEventValidator,
2840
);
2941
}
3042

@@ -36,7 +48,7 @@ export class EventPublisherFacade {
3648
);
3749
this.messageRequestRejectedEventPublisher.sendEvents<MessageRequestRejected>(
3850
[event],
39-
messageRequestRejectedValidator,
51+
rejectedEventValidator,
4052
);
4153
}
4254

@@ -48,7 +60,7 @@ export class EventPublisherFacade {
4860
);
4961
this.messageRequestSkippedEventPublisher.sendEvents<MessageRequestSkipped>(
5062
[event],
51-
messageRequestSkippedValidator,
63+
skippedEventValidator,
5264
);
5365
}
5466
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { expect, test } from '@playwright/test';
2+
import { CSI, ENV } from 'constants/backend-constants';
3+
import { PDMResourceAvailable } from 'digital-letters-events';
4+
import messagePDMResourceAvailableValidator from 'digital-letters-events/PDMResourceAvailable.js';
5+
import { getLogsFromCloudwatch } from 'helpers/cloudwatch-helpers';
6+
import { getTtl } from 'helpers/dynamodb-helpers';
7+
import eventPublisher from 'helpers/event-bus-helpers';
8+
import expectToPassEventually from 'helpers/expectations';
9+
import { expectMessageContainingString, purgeQueue } from 'helpers/sqs-helpers';
10+
import {SenderManagement} from 'sender-management';
11+
import { v4 as uuidv4 } from 'uuid';
12+
import { ParameterStoreCache } from 'utils';
13+
14+
const senderIdInvokingNotify = 'componentTestSender_RoutingConfig';
15+
const senderIdThatSkipsNotify = 'componentTestSender_NoRoutingConfig';
16+
const EVENT_BUS_LOG_GROUP_NAME = `/aws/vendedlogs/events/event-bus/${ENV}`;
17+
const CORE_NOTIFIER_LAMBDA_LOG_GROUP_NAME = `/aws/lambda/${CSI}-core-notifier`;
18+
19+
test.describe('Digital Letters - Core Notify', () => {
20+
const handleCoreNotifierDlqName = `${CSI}-core-notifier-errors-queue`;
21+
const parameterStore = new ParameterStoreCache();
22+
const senderManagement = SenderManagement({
23+
parameterStore,
24+
});
25+
26+
async function deleteSendersIfExist(){
27+
senderManagement.deleteSender({senderId: senderIdInvokingNotify});
28+
senderManagement.deleteSender({senderId: senderIdThatSkipsNotify});
29+
};
30+
31+
test.beforeAll(async () => {
32+
await purgeQueue(handleCoreNotifierDlqName);
33+
await deleteSendersIfExist();
34+
senderManagement.putSender({
35+
senderId: senderIdInvokingNotify,
36+
senderName: 'componentTestSender_RoutingConfig',
37+
meshMailboxSenderId: 'meshMailboxSender1',
38+
meshMailboxReportsId: 'meshMailboxReports1',
39+
routingConfigId: 'routing-config-1',
40+
fallbackWaitTimeSeconds: 100,
41+
});
42+
43+
senderManagement.putSender({
44+
senderId: senderIdThatSkipsNotify,
45+
senderName: 'componentTestSender_WithoutRoutingConfig',
46+
meshMailboxSenderId: 'meshMailboxSender2',
47+
meshMailboxReportsId: 'meshMailboxReports2',
48+
fallbackWaitTimeSeconds: 100,
49+
});
50+
});
51+
52+
test.afterAll(async () => {
53+
await purgeQueue(handleCoreNotifierDlqName);
54+
await deleteSendersIfExist();
55+
});
56+
57+
58+
test('given PDMResourceAvailable event, when client has routingConfigId then a message is sent to core Notify', async () => {
59+
const letterId = uuidv4();
60+
61+
await eventPublisher.sendEvents<PDMResourceAvailable>(
62+
[
63+
{
64+
id: letterId,
65+
specversion: '1.0',
66+
source:
67+
'/nhs/england/notify/production/primary/data-plane/digitalletters/pdm',
68+
subject:
69+
'customer/920fca11-596a-4eca-9c47-99f624614658/recipient/769acdd4-6a47-496f-999f-76a6fd2c3959',
70+
type: 'uk.nhs.notify.digital.letters.pdm.resource.available.v1',
71+
time: '2023-06-20T12:00:00Z',
72+
recordedtime: '2023-06-20T12:00:00.250Z',
73+
severitynumber: 2,
74+
traceparent:
75+
'00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
76+
datacontenttype: 'application/json',
77+
dataschema:
78+
'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10-draft/data/digital-letters-pdm-resource-available-data.schema.json',
79+
severitytext: 'INFO',
80+
data: {
81+
messageReference: 'ref1',
82+
senderId: senderIdInvokingNotify,
83+
resourceId: 'resource-123',
84+
nhsNumber: '9991234566',
85+
odsCode: 'A12345',
86+
},
87+
},
88+
],
89+
messagePDMResourceAvailableValidator,
90+
);
91+
92+
// Verify the event happ
93+
await expectToPassEventually(async () => {
94+
const filteredLogs = await getLogsFromCloudwatch(
95+
CORE_NOTIFIER_LAMBDA_LOG_GROUP_NAME,
96+
[
97+
'$.message.description = "1 of 1 records processed successfully"',
98+
],
99+
);
100+
101+
expect(filteredLogs.length).toEqual(1);
102+
}, 120);
103+
// more assertions needed, i.e. the event published
104+
});
105+
// create following tests
106+
// when the sender has no routingConfigId then the Skipped message is published
107+
// when fails repeatedly then goes to DLQ
108+
});

0 commit comments

Comments
 (0)