Skip to content

Commit 75f1f5f

Browse files
committed
CCM-13278: Add component tests for MESH acknowledger
1 parent 916db23 commit 75f1f5f

6 files changed

Lines changed: 235 additions & 1 deletion

File tree

infrastructure/terraform/components/dl/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ No requirements.
4949
| <a name="module_pdm_uploader"></a> [pdm\_uploader](#module\_pdm\_uploader) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
5050
| <a name="module_s3bucket_cf_logs"></a> [s3bucket\_cf\_logs](#module\_s3bucket\_cf\_logs) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-s3bucket.zip | n/a |
5151
| <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 |
52+
| <a name="module_s3bucket_non_pii_data"></a> [s3bucket\_non\_pii\_data](#module\_s3bucket\_non\_pii\_data) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-s3bucket.zip | n/a |
5253
| <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 |
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_mesh_acknowledge"></a> [sqs\_mesh\_acknowledge](#module\_sqs\_mesh\_acknowledge) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-sqs.zip | n/a |
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
module "s3bucket_non_pii_data" {
2+
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-s3bucket.zip"
3+
4+
name = "non-pii-data"
5+
6+
aws_account_id = var.aws_account_id
7+
region = var.region
8+
project = var.project
9+
environment = var.environment
10+
component = local.component
11+
12+
kms_key_arn = module.kms.key_arn
13+
14+
policy_documents = [data.aws_iam_policy_document.s3bucket_non_pii_data.json]
15+
}
16+
17+
data "aws_iam_policy_document" "s3bucket_non_pii_data" {
18+
statement {
19+
sid = "AllowManagedAccountsToList"
20+
effect = "Allow"
21+
22+
actions = [
23+
"s3:ListBucket",
24+
]
25+
26+
resources = [
27+
module.s3bucket_non_pii_data.arn,
28+
]
29+
30+
principals {
31+
type = "AWS"
32+
identifiers = [
33+
"arn:aws:iam::${var.aws_account_id}:root"
34+
]
35+
}
36+
}
37+
38+
statement {
39+
sid = "AllowManagedAccountsToGet"
40+
effect = "Allow"
41+
42+
actions = [
43+
"s3:GetObject",
44+
"s3:PutObject",
45+
]
46+
47+
resources = [
48+
"${module.s3bucket_non_pii_data.arn}/*",
49+
]
50+
51+
principals {
52+
type = "AWS"
53+
identifiers = [
54+
"arn:aws:iam::${var.aws_account_id}:root"
55+
]
56+
}
57+
}
58+
}

tests/playwright/constants/backend-constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const TTL_POLL_LAMBDA_NAME = `${CSI}-ttl-poll`;
1616
export const TTL_QUEUE_NAME = `${CSI}-ttl-queue`;
1717
export const TTL_DLQ_NAME = `${CSI}-ttl-dlq`;
1818
export const PDM_UPLOADER_DLQ_NAME = `${CSI}-pdm-uploader-dlq`;
19+
export const MESH_ACKNOWLEDGE_DLQ_NAME = `${CSI}-mesh-acknowledge-dlq`;
1920

2021
// Queue Url Prefix
2122
export const SQS_URL_PREFIX = `https://sqs.${REGION}.amazonaws.com/${AWS_ACCOUNT_ID}/`;
@@ -30,6 +31,7 @@ export const TTL_TABLE_NAME = `${CSI}-ttl`;
3031

3132
// S3
3233
export const LETTERS_S3_BUCKET_NAME = `nhs-${process.env.AWS_ACCOUNT_ID}-${REGION}-${ENV}-dl-letters`;
34+
export const NON_PII_S3_BUCKET_NAME = `nhs-${process.env.AWS_ACCOUNT_ID}-${REGION}-${ENV}-dl-non-pii-data`;
3335

3436
// Cloudwatch
3537
export const PDM_UPLOADER_LAMBDA_LOG_GROUP_NAME = `/aws/lambda/${CSI}-pdm-uploader`;
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { expect, test } from '@playwright/test';
2+
import {
3+
ENV,
4+
MESH_ACKNOWLEDGE_DLQ_NAME,
5+
NON_PII_S3_BUCKET_NAME,
6+
} from 'constants/backend-constants';
7+
import { MESHInboxMessageDownloaded } from 'digital-letters-events';
8+
import messageDownloadedValidator from 'digital-letters-events/MESHInboxMessageDownloaded.js';
9+
import { getLogsFromCloudwatch } from 'helpers/cloudwatch-helpers';
10+
import eventPublisher from 'helpers/event-bus-helpers';
11+
import expectToPassEventually from 'helpers/expectations';
12+
import { downloadFromS3 } from 'helpers/s3-helpers';
13+
import { expectMessageContainingString } from 'helpers/sqs-helpers';
14+
import { v4 as uuidv4 } from 'uuid';
15+
16+
test.describe('Digital Letters - Mesh Acknowledger', () => {
17+
// These values match the ones configured in senders.setup.ts
18+
const senderId = 'test-sender-1';
19+
const sendersMeshMailboxId = 'test-mesh-sender-1';
20+
21+
const validMessageDownloadedEvent: MESHInboxMessageDownloaded = {
22+
id: uuidv4(),
23+
specversion: '1.0',
24+
source:
25+
'/nhs/england/notify/production/primary/data-plane/digitalletters/mesh',
26+
subject:
27+
'customer/920fca11-596a-4eca-9c47-99f624614658/recipient/769acdd4-6a47-496f-999f-76a6fd2c3959',
28+
type: 'uk.nhs.notify.digital.letters.mesh.inbox.message.downloaded.v1',
29+
time: '2023-06-20T12:00:00Z',
30+
recordedtime: '2023-06-20T12:00:00.250Z',
31+
severitynumber: 2,
32+
traceparent: '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
33+
datacontenttype: 'application/json',
34+
dataschema:
35+
'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10-draft/data/digital-letters-mesh-inbox-message-downloaded-data.schema.json',
36+
severitytext: 'INFO',
37+
datacategory: 'non-sensitive',
38+
dataclassification: 'public',
39+
dataregulation: 'GDPR',
40+
tracestate: 'rojo=00f067aa0ba902b7,congo=t61rcWkgMzE',
41+
partitionkey: 'customer-920fca11',
42+
sampledrate: 5,
43+
sequence: '00000000000000000042',
44+
data: {
45+
meshMessageId: '12345',
46+
messageUri: `https://example.com/ttl/resource/${uuidv4()}`,
47+
messageReference: 'ref1',
48+
senderId,
49+
},
50+
};
51+
52+
test('should send MESH acknowledgement and publish message acknowledged event following message downloaded event', async () => {
53+
const letterId = uuidv4();
54+
const messageReference = uuidv4();
55+
const meshMessageId = '20200601122152994285_D59900';
56+
57+
await eventPublisher.sendEvents<MESHInboxMessageDownloaded>(
58+
[
59+
{
60+
...validMessageDownloadedEvent,
61+
id: letterId,
62+
data: {
63+
...validMessageDownloadedEvent.data,
64+
messageUri: `https://example.com/ttl/resource/${letterId}`,
65+
messageReference,
66+
meshMessageId,
67+
},
68+
},
69+
],
70+
messageDownloadedValidator,
71+
);
72+
73+
// The mailbox ID matches the Mock MESH config in SSM.
74+
const meshMailboxId = 'mock-mailbox';
75+
76+
// Verify MESH acknowledgement message was published.
77+
await expectToPassEventually(async () => {
78+
const messageContent = await downloadFromS3(
79+
NON_PII_S3_BUCKET_NAME,
80+
`mock-mesh/${meshMailboxId}/out/${sendersMeshMailboxId}/${messageReference}_`,
81+
);
82+
83+
const messageHeaders = messageContent.metadata ?? {};
84+
expect(messageHeaders.subject).toEqual('202');
85+
expect(messageHeaders.local_id).toEqual(messageReference);
86+
expect(messageHeaders.workflow_id).toEqual('NHS_NOTIFY_SEND_REQUEST_ACK');
87+
88+
const messageBody = JSON.parse(messageContent.body);
89+
expect(messageBody).toEqual({
90+
meshMessageId,
91+
requestId: `${senderId}_${messageReference}`,
92+
});
93+
});
94+
95+
// Verify message acknowledged event was published.
96+
await expectToPassEventually(async () => {
97+
const eventLogEntry = await getLogsFromCloudwatch(
98+
`/aws/vendedlogs/events/event-bus/nhs-${ENV}-dl`,
99+
[
100+
'$.message_type = "EVENT_RECEIPT"',
101+
'$.details.detail_type = "uk.nhs.notify.digital.letters.mesh.inbox.message.acknowledged.v1"',
102+
`$.details.event_detail = "*\\"messageReference\\":\\"${messageReference}\\"*"`,
103+
`$.details.event_detail = "*\\"senderId\\":\\"${senderId}\\"*"`,
104+
`$.details.event_detail = "*\\"meshMailboxId\\":\\"${sendersMeshMailboxId}\\"*"`,
105+
],
106+
);
107+
108+
expect(eventLogEntry.length).toEqual(1);
109+
});
110+
});
111+
112+
test('should send invalid event to dlq', async () => {
113+
const letterId = uuidv4();
114+
115+
await eventPublisher.sendEvents<
116+
MESHInboxMessageDownloaded & { data: { unexpectedField: string } }
117+
>(
118+
[
119+
{
120+
...validMessageDownloadedEvent,
121+
id: letterId,
122+
data: {
123+
...validMessageDownloadedEvent.data,
124+
unexpectedField: 'I should not be here',
125+
},
126+
},
127+
],
128+
messageDownloadedValidator,
129+
);
130+
131+
await expectMessageContainingString(MESH_ACKNOWLEDGE_DLQ_NAME, letterId);
132+
});
133+
});

tests/playwright/helpers/expectations.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,13 @@ async function expectToPassEventually<R>(
5050
} catch (error: unknown) {
5151
latestCaughtObjects.delete(invocationToken);
5252
if (Date.now() - startTime > timeout * 1000) {
53+
// eslint-disable-next-line no-console
5354
console.log('Failed to finish test in time', {
5455
now: new Date().toISOString(),
5556
timeout,
5657
delay,
5758
});
59+
// eslint-disable-next-line no-console
5860
console.error(error);
5961
throw error;
6062
} else {

tests/playwright/helpers/s3-helpers.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {
2+
GetObjectCommand,
23
ListBucketsCommand,
4+
ListObjectsV2Command,
35
PutObjectCommand,
46
S3Client,
57
} from '@aws-sdk/client-s3';
@@ -36,4 +38,40 @@ async function uploadToS3(
3638
);
3739
}
3840

39-
export { listBuckets, uploadToS3 };
41+
async function downloadFromS3(
42+
bucket: string,
43+
keyPrefix: string,
44+
): Promise<{ body: string; metadata?: Record<string, string> }> {
45+
const objects = await s3.send(
46+
new ListObjectsV2Command({ Bucket: bucket, Prefix: keyPrefix }),
47+
);
48+
49+
if ((objects.Contents?.length ?? 0) > 1) {
50+
throw new Error(
51+
`Multiple objects found for prefix s3://${bucket}/${keyPrefix}`,
52+
);
53+
}
54+
55+
if ((objects.Contents?.length ?? 0) === 0) {
56+
throw new Error(`No objects found for prefix s3://${bucket}/${keyPrefix}`);
57+
}
58+
59+
const key = objects.Contents?.[0]?.Key;
60+
const response = await s3.send(
61+
new GetObjectCommand({
62+
Bucket: bucket,
63+
Key: key,
64+
}),
65+
);
66+
67+
if (!response.Body) {
68+
throw new Error(`No content found for s3://${bucket}/${key}`);
69+
}
70+
71+
return {
72+
body: await response.Body.transformToString(),
73+
metadata: response.Metadata,
74+
};
75+
}
76+
77+
export { downloadFromS3, listBuckets, uploadToS3 };

0 commit comments

Comments
 (0)