Skip to content

Commit 1054505

Browse files
committed
CCM-12616: add messageReference to the event data
1 parent 0e51120 commit 1054505

File tree

5 files changed

+87
-20
lines changed

5 files changed

+87
-20
lines changed

lambdas/mesh-download/src/__tests__/test_processor.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def create_valid_cloud_event():
4545
'data': {
4646
'meshMessageId': 'test_message_123',
4747
'senderId': 'TEST_SENDER',
48-
'workflowId': 'TEST_WORKFLOW'
48+
'messageReference': 'ref_001'
4949
}
5050
}
5151

@@ -419,3 +419,44 @@ def getenv_side_effect(key, default=''):
419419
assert len(published_events) == 1
420420
message_uri = published_events[0]['data']['messageUri']
421421
assert message_uri.startswith('s3://test-pii-bucket/')
422+
423+
@patch('src.processor.os.getenv')
424+
def test_message_reference_mismatch_raises_error(self, mock_getenv):
425+
"""When messageReference from event doesn't match local_id from MESH, should raise ValueError"""
426+
from src.processor import MeshDownloadProcessor
427+
428+
mesh_client, log, s3_client, event_publisher, document_store = setup_mocks()
429+
430+
def getenv_side_effect(key, default=None):
431+
return {
432+
'EVENT_PUBLISHER_EVENT_BUS_ARN': 'arn:aws:events:test',
433+
'EVENT_PUBLISHER_DLQ_URL': 'https://sqs.test.com/dlq',
434+
'ENVIRONMENT': 'development',
435+
'DEPLOYMENT': 'dev-1',
436+
'PII_BUCKET': 'test-pii-bucket',
437+
'MOCK_MESH_BUCKET': 'test-mock-bucket'
438+
}.get(key, default)
439+
440+
mock_getenv.side_effect = getenv_side_effect
441+
442+
processor = MeshDownloadProcessor(
443+
mesh_client=mesh_client,
444+
log=log,
445+
s3_client=s3_client,
446+
document_store=document_store,
447+
event_publisher=event_publisher
448+
)
449+
450+
# Create MESH message with different local_id than event messageReference
451+
mesh_message = create_mesh_message(local_id='different_ref')
452+
mesh_client.retrieve_message.return_value = mesh_message
453+
454+
# Event has messageReference='ref_001' (from create_valid_cloud_event)
455+
sqs_record = create_sqs_record()
456+
457+
with pytest.raises(ValueError, match="messageReference mismatch"):
458+
processor.process_sqs_message(sqs_record)
459+
460+
# Should not acknowledge or publish if validation fails
461+
mesh_message.acknowledge.assert_not_called()
462+
event_publisher.send_events.assert_not_called()

lambdas/mesh-download/src/processor.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ def process_sqs_message(self, sqs_record):
104104
data = validated_event.data
105105
mesh_message_id = data.meshMessageId
106106
sender_id = data.senderId
107+
message_reference = data.messageReference
107108

108109
if not mesh_message_id:
109110
self.__log.error("Missing meshMessageId in event data", sqs_record=sqs_record)
@@ -113,15 +114,15 @@ def process_sqs_message(self, sqs_record):
113114
logger.info("Processing MESH download request")
114115

115116
# Download and store the MESH message
116-
self.download_and_store_message(mesh_message_id, sender_id, logger)
117+
self.download_and_store_message(mesh_message_id, sender_id, message_reference, logger)
117118

118119
except Exception as exc:
119120
self.__log.error("Error processing SQS message",
120121
error=str(exc),
121122
sqs_record=sqs_record)
122123
raise
123124

124-
def download_and_store_message(self, mesh_message_id, sender_id, logger):
125+
def download_and_store_message(self, mesh_message_id, sender_id, message_reference, logger):
125126
"""
126127
Downloads a MESH message and stores it in S3
127128
"""
@@ -135,7 +136,7 @@ def download_and_store_message(self, mesh_message_id, sender_id, logger):
135136

136137
# Extract data from MESH message headers
137138
sender_mailbox_id = getattr(message, 'sender', '')
138-
message_reference = getattr(message, 'local_id', '')
139+
local_id = getattr(message, 'local_id', '')
139140
message_type = getattr(message, 'message_type', '')
140141
subject = getattr(message, 'subject', '')
141142
workflow_id = getattr(message, 'workflow_id', '')
@@ -146,10 +147,22 @@ def download_and_store_message(self, mesh_message_id, sender_id, logger):
146147
sender=sender_mailbox_id,
147148
workflow_id=workflow_id,
148149
subject=subject,
149-
local_id=message_reference,
150-
message_type=message_type
150+
local_id=local_id,
151+
message_type=message_type,
152+
message_reference_from_event=message_reference
151153
)
152154

155+
# Validate messageReference matches local_id from MESH
156+
if message_reference != local_id:
157+
logger.warning(
158+
"messageReference mismatch between event and MESH message",
159+
expected=message_reference,
160+
actual=local_id
161+
)
162+
raise ValueError(
163+
f"messageReference mismatch: event has '{message_reference}' but MESH message has '{local_id}'"
164+
)
165+
153166
logger.info("Processing message based on type")
154167

155168
# Process DATA message (document reference)

lambdas/mesh-poll/src/processor.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,14 +94,14 @@ def process_message(self, message):
9494
sender_mailbox_id = getattr(message, "sender", "")
9595
workflow_id = getattr(message, "workflow_id", "")
9696
subject = getattr(message, "subject", "")
97-
local_id = getattr(message, "local_id", "")
97+
message_reference = getattr(message, "local_id", "")
9898

9999
logger = self.__log.bind(
100100
message_id=message.id(),
101101
sender=sender_mailbox_id,
102102
workflow_id=workflow_id,
103103
subject=subject,
104-
local_id=local_id,
104+
local_id=message_reference,
105105
message_type=message_type,
106106
)
107107

@@ -121,7 +121,8 @@ def process_message(self, message):
121121
event_detail = {
122122
"data": {
123123
"meshMessageId": message_id,
124-
"senderId": client_id
124+
"senderId": client_id,
125+
"messageReference": message_reference
125126
}
126127
}
127128

utils/event-publisher-py/event_publisher/__tests__/test_event_publisher.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,18 @@ def valid_cloud_event():
4646
'specversion': '1.0',
4747
'source': '/nhs/england/notify/production/primary/data-plane/digitalletters/mesh',
4848
'subject': 'customer/920fca11-596a-4eca-9c47-99f624614658/recipient/769acdd4-6a47-496f-999f-76a6fd2c3959',
49-
'type': 'uk.nhs.notify.digital.letters.sent.v1',
49+
'type': 'uk.nhs.notify.digital.letters.mesh.inbox.message.received.v1',
5050
'time': '2023-06-20T12:00:00Z',
5151
'recordedtime': '2023-06-20T12:00:00.250Z',
5252
'severitynumber': 2,
53+
'severitytext': 'INFO',
5354
'traceparent': '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
54-
'dataschema': 'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10/digital-letter-base-data.schema.json',
55+
'dataschema': 'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10/digital-letters-mesh-inbox-message-received-data.schema.json',
56+
'dataschemaversion': '1.0',
5557
'data': {
56-
'digital-letter-id': '123e4567-e89b-12d3-a456-426614174000',
57-
'messageReference': 'ref1',
58+
'meshMessageId': 'test-123',
5859
'senderId': 'sender1',
60+
'messageReference': 'ref_001'
5961
},
6062
}
6163

@@ -67,18 +69,19 @@ def valid_cloud_event2():
6769
'profilepublished': '2025-10',
6870
'id': '550e8400-e29b-41d4-a716-446655440002',
6971
'specversion': '1.0',
70-
'source': '/nhs/england/notify/development/primary/data-plane/digital-letters',
72+
'source': '/nhs/england/notify/development/primary/data-plane/digitalletters/mesh',
7173
'subject': 'customer/920fca11-596a-4eca-9c47-99f624614658/recipient/769acdd4-6a47-496f-999f-76a6fd2c3959',
72-
'type': 'uk.nhs.notify.digital.letters.sent.v2',
74+
'type': 'uk.nhs.notify.digital.letters.mesh.inbox.message.received.v1',
7375
'time': '2023-06-20T12:00:00Z',
7476
'recordedtime': '2023-06-20T12:00:00.250Z',
7577
'severitynumber': 2,
7678
'traceparent': '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
77-
'dataschema': 'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10/digital-letter-base-data.schema.json',
79+
'dataschema': 'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10/digital-letters-mesh-inbox-message-received-data.schema.json',
80+
'dataschemaversion': '1.0',
7881
'data': {
79-
'digital-letter-id': '123e4567-e89b-12d3-a456-426614174001',
80-
'messageReference': 'ref1',
82+
'meshMessageId': 'test-123',
8183
'senderId': 'sender1',
84+
'messageReference': 'ref_001'
8285
},
8386
}
8487

@@ -101,11 +104,14 @@ def test_should_return_empty_array_when_no_events_provided(self, test_config, mo
101104
mock_events_client.put_events.assert_not_called()
102105
mock_sqs_client.send_message_batch.assert_not_called()
103106

104-
def test_should_send_valid_events_to_eventbridge(self, test_config, mock_events_client, valid_cloud_event, valid_cloud_event2):
107+
def test_should_send_valid_events_to_eventbridge(self, test_config, mock_events_client, mock_sqs_client, valid_cloud_event, valid_cloud_event2):
105108
mock_events_client.put_events.return_value = {
106109
'FailedEntryCount': 0,
107110
'Entries': [{'EventId': 'event-1'}]
108111
}
112+
mock_sqs_client.send_message_batch.return_value = {
113+
'Successful': []
114+
}
109115

110116
publisher = EventPublisher(**test_config)
111117
result = publisher.send_events([valid_cloud_event, valid_cloud_event2])
@@ -154,6 +160,7 @@ def test_should_send_failed_eventbridge_events_to_dlq(self, test_config, mock_ev
154160

155161
assert result == []
156162
assert mock_events_client.put_events.call_count == 1
163+
# Should call DLQ once for the failed event
157164
assert mock_sqs_client.send_message_batch.call_count == 1
158165

159166
# Verify EventBridge was called with both events
@@ -180,6 +187,7 @@ def test_should_handle_eventbridge_send_error_and_send_all_events_to_dlq(self, t
180187

181188
assert result == []
182189
assert mock_events_client.put_events.call_count == 1
190+
# Should call DLQ once for all events after EventBridge failure
183191
assert mock_sqs_client.send_message_batch.call_count == 1
184192

185193
def test_should_return_failed_events_when_dlq_also_fails(self, test_config, mock_sqs_client, invalid_cloud_event):
@@ -280,11 +288,14 @@ def test_should_throw_error_when_dlq_url_is_missing(self, test_config):
280288
with pytest.raises(ValueError, match='dlq_url has not been specified'):
281289
EventPublisher(**test_config)
282290

283-
def test_should_be_reusable_for_multiple_calls(self, test_config, mock_events_client, valid_cloud_event, valid_cloud_event2):
291+
def test_should_be_reusable_for_multiple_calls(self, test_config, mock_events_client, mock_sqs_client, valid_cloud_event, valid_cloud_event2):
284292
mock_events_client.put_events.return_value = {
285293
'FailedEntryCount': 0,
286294
'Entries': [{'EventId': 'event-1'}]
287295
}
296+
mock_sqs_client.send_message_batch.return_value = {
297+
'Successful': []
298+
}
288299

289300
publisher = EventPublisher(**test_config)
290301

utils/event-publisher-py/event_publisher/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ class MeshInboxMessageData(BaseModel):
174174
"""Data payload for MESH inbox message received event"""
175175
meshMessageId: str = Field(..., min_length=1)
176176
senderId: str = Field(..., min_length=1)
177+
messageReference: str = Field(..., min_length=1)
177178

178179

179180
class MeshInboxMessageEvent(CloudEvent):

0 commit comments

Comments
 (0)