Skip to content

Commit bfa5fe0

Browse files
authored
CCM-14482: Send nack back to trust for MESHInboxMessageInvalid events (#284)
* CCM-14482: send nack back to trust for MESHInboxMessageInvalid events * CCM-14482: fall back to no-local-id when local_id is None * CCM-14482: Update mesh mock to return a meshMessageId * CCM-14482: Fix linting issue * CCM-14482: Fix component tests * CCM-14482: Refactor event tests to reduce duplication * CCM-14482: Update fixtures" * CCM-14482: Fix unit tests
1 parent dce9b6a commit bfa5fe0

26 files changed

Lines changed: 1008 additions & 139 deletions

infrastructure/terraform/components/dl/cloudwatch_event_rule_mesh_inbox_message_downloaded.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ resource "aws_cloudwatch_event_target" "pdm_uploader_target" {
2626
event_bus_name = aws_cloudwatch_event_bus.main.name
2727
}
2828

29-
resource "aws_cloudwatch_event_target" "mesh_cknowledge_target" {
29+
resource "aws_cloudwatch_event_target" "mesh_acknowledge_target" {
3030
rule = aws_cloudwatch_event_rule.mesh_inbox_message_downloaded.name
3131
arn = module.sqs_mesh_acknowledge.sqs_queue_arn
3232
target_id = "mesh_acknowledge_target"
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
resource "aws_cloudwatch_event_rule" "mesh_inbox_message_invalid" {
2+
name = "${local.csi}-mesh-inbox-message-invalid"
3+
description = "MESH inbox message invalid event rule"
4+
event_bus_name = aws_cloudwatch_event_bus.main.name
5+
6+
event_pattern = jsonencode({
7+
"detail" : {
8+
"type" : [
9+
"uk.nhs.notify.digital.letters.mesh.inbox.message.invalid.v1"
10+
],
11+
}
12+
})
13+
}
14+
15+
resource "aws_cloudwatch_event_target" "mesh_acknowledge_invalid_target" {
16+
rule = aws_cloudwatch_event_rule.mesh_inbox_message_invalid.name
17+
arn = module.sqs_mesh_acknowledge.sqs_queue_arn
18+
target_id = "mesh_acknowledge_invalid_target"
19+
event_bus_name = aws_cloudwatch_event_bus.main.name
20+
}

infrastructure/terraform/components/dl/module_sqs_mesh_acknowledge.tf

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ data "aws_iam_policy_document" "sqs_mesh_acknowledge" {
3535
condition {
3636
test = "ArnLike"
3737
variable = "aws:SourceArn"
38-
values = [aws_cloudwatch_event_rule.mesh_inbox_message_downloaded.arn]
38+
39+
values = [
40+
aws_cloudwatch_event_rule.mesh_inbox_message_downloaded.arn,
41+
aws_cloudwatch_event_rule.mesh_inbox_message_invalid.arn,
42+
]
3943
}
4044
}
4145
}

infrastructure/terraform/components/dl/s3_object_failure_codes.tf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
resource "aws_s3_object" "failure_codes" {
22
bucket = module.s3bucket_reporting.bucket
33
key = "reference-data/failure_codes/failure_codes.csv"
4-
source = "${path.module}/data/failure_codes.csv"
4+
source = "${path.module}/../../../../utils/py-utils/dl_utils/failure_codes.csv"
55
content_type = "text/csv"
6-
etag = filemd5("${path.module}/data/failure_codes.csv")
6+
etag = filemd5("${path.module}/../../../../utils/py-utils/dl_utils/failure_codes.csv")
77

88
tags = merge(
99
local.default_tags,

lambdas/mesh-acknowledge/mesh_acknowledge/__tests__/fixtures.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@ def create_downloaded_event_dict(event_id: str) -> Dict[str, str | int | Dict[st
77
return {
88
"id": event_id,
99
"specversion": "1.0",
10-
"source": (
11-
"/nhs/england/notify/production/primary/"
12-
'digitalletters/mesh'
13-
),
10+
"source": "/nhs/england/notify/production/primary/digitalletters/mesh",
1411
"subject": (
1512
'customer/920fca11-596a-4eca-9c47-99f624614658/'
1613
'recipient/769acdd4-6a47-496f-999f-76a6fd2c3959'
@@ -40,3 +37,43 @@ def create_downloaded_event_dict(event_id: str) -> Dict[str, str | int | Dict[st
4037
"senderId": "SENDER001",
4138
}
4239
}
40+
41+
42+
def create_invalid_event_dict(event_id: str, message_reference: str | None = "REF123") -> Dict[str, str | int | Dict[str, str]]:
43+
"""Create a dictionary representing a MESHInboxMessageInvalid event"""
44+
data: Dict[str, str] = {
45+
"meshMessageId": "MSG123456",
46+
"senderId": "SENDER001",
47+
"failureCode": "DL_CLIV_005",
48+
}
49+
if message_reference is not None:
50+
data["messageReference"] = message_reference
51+
52+
return {
53+
"id": event_id,
54+
"specversion": "1.0",
55+
"source": "/nhs/england/notify/production/primary/digitalletters/mesh",
56+
"subject": (
57+
'customer/920fca11-596a-4eca-9c47-99f624614658/'
58+
'recipient/769acdd4-6a47-496f-999f-76a6fd2c3959'
59+
),
60+
"type": (
61+
'uk.nhs.notify.digital.letters.mesh.inbox.message.invalid.v1'
62+
),
63+
"plane": "data",
64+
"time": '2026-01-08T10:00:00Z',
65+
"recordedtime": '2026-01-08T10:00:00Z',
66+
"severitynumber": 4,
67+
"severitytext": 'ERROR',
68+
"traceparent": '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
69+
"datacontenttype": 'application/json',
70+
"dataschema": (
71+
'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10-draft/data/'
72+
'digital-letters-mesh-inbox-message-invalid-data.schema.json'
73+
),
74+
"dataschemaversion": '1.0.0',
75+
"datacategory": "non-sensitive",
76+
"dataclassification": "public",
77+
"dataregulation": "GDPR",
78+
"data": data,
79+
}

lambdas/mesh-acknowledge/mesh_acknowledge/__tests__/test_acknowledger.py

Lines changed: 73 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
from mesh_acknowledge.acknowledger import (
88
MeshAcknowledger,
99
NOTIFY_ACK_WORKFLOW_ID,
10-
ACK_SUBJECT
10+
ACK_SUBJECT,
11+
NACK_SUBJECT,
1112
)
1213

1314
SENT_MESH_MESSAGE_ID = "MSG123456"
@@ -71,25 +72,6 @@ def test_acknowledge_message_sends_correct_message(
7172
subject=ACK_SUBJECT
7273
)
7374

74-
def test_acknowledge_message_returns_ack_id(
75-
self, acknowledger, mock_mesh_client
76-
):
77-
"""Test that acknowledge_message returns the acknowledgment ID"""
78-
mailbox_id = "MAILBOX001"
79-
message_id = "MSG123456"
80-
message_reference = "REF789"
81-
sender_id = "SENDER001"
82-
83-
expected_ack_id = "ACK_CUSTOM_ID"
84-
85-
mock_mesh_client.send_message.return_value = expected_ack_id
86-
87-
ack_message_id = acknowledger.acknowledge_message(
88-
mailbox_id, message_id, message_reference, sender_id
89-
)
90-
91-
assert ack_message_id == expected_ack_id
92-
9375
def test_acknowledge_message_raises_error_if_mesh_send_fails(
9476
self, acknowledger, mock_mesh_client
9577
):
@@ -107,3 +89,74 @@ def test_acknowledge_message_raises_error_if_mesh_send_fails(
10789
acknowledger.acknowledge_message(
10890
mailbox_id, message_id, message_reference, sender_id
10991
)
92+
93+
94+
class TestMeshAcknowledgerNack:
95+
"""Test suite for MeshAcknowledger.negative_acknowledge_message"""
96+
97+
def test_negative_acknowledge_message_sends_correct_message_with_reference(
98+
self, acknowledger, mock_mesh_client
99+
):
100+
"""Test that negative_acknowledge_message sends the correct NACK with messageReference"""
101+
mailbox_id = "MAILBOX001"
102+
message_id = "MSG123456"
103+
failure_code = "DL_CLIV_004"
104+
sender_id = "SENDER001"
105+
message_reference = "REF789"
106+
107+
expected_body = json.dumps({
108+
"meshMessageId": message_id,
109+
"failureCode": failure_code,
110+
"requestId": f"{sender_id}_{message_reference}",
111+
"message": "Duplicate request",
112+
}).encode()
113+
114+
acknowledger.negative_acknowledge_message(
115+
mailbox_id, message_id, failure_code, sender_id, message_reference
116+
)
117+
118+
mock_mesh_client.send_message.assert_called_once_with(
119+
mailbox_id,
120+
expected_body,
121+
workflow_id=NOTIFY_ACK_WORKFLOW_ID,
122+
local_id=message_reference,
123+
subject=NACK_SUBJECT
124+
)
125+
126+
def test_negative_acknowledge_message_sends_correct_message_without_reference(
127+
self, acknowledger, mock_mesh_client
128+
):
129+
"""Test that negative_acknowledge_message sends the correct NACK without messageReference"""
130+
mailbox_id = "MAILBOX001"
131+
message_id = "MSG123456"
132+
failure_code = "DL_CLIV_005"
133+
sender_id = "SENDER001"
134+
135+
expected_body = json.dumps({
136+
"meshMessageId": message_id,
137+
"failureCode": failure_code,
138+
"requestId": f"{sender_id}_",
139+
"message": "Invalid FHIR resource",
140+
}).encode()
141+
142+
acknowledger.negative_acknowledge_message(
143+
mailbox_id, message_id, failure_code, sender_id
144+
)
145+
146+
mock_mesh_client.send_message.assert_called_once_with(
147+
mailbox_id,
148+
expected_body,
149+
workflow_id=NOTIFY_ACK_WORKFLOW_ID,
150+
subject=NACK_SUBJECT
151+
)
152+
153+
def test_negative_acknowledge_message_unknown_failure_code_omits_description(
154+
self, acknowledger, mock_mesh_client
155+
):
156+
"""Test that an unknown failure code omits message from the body"""
157+
acknowledger.negative_acknowledge_message(
158+
"MAILBOX001", "MSG123456", "UNKNOWN_CODE", "SENDER001"
159+
)
160+
161+
call_body = json.loads(mock_mesh_client.send_message.call_args[0][1].decode())
162+
assert "message" not in call_body

0 commit comments

Comments
 (0)