Skip to content

Commit b1a6b60

Browse files
committed
CCM-13278: Implement mesh-acknowledge lambda
1 parent 92f86cc commit b1a6b60

16 files changed

Lines changed: 1612 additions & 28 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Fixtures for tests"""
2+
from typing import Dict
3+
4+
5+
def create_downloaded_event_dict(event_id: str) -> Dict[str, str | int | Dict[str, str]]:
6+
"""Create a dictionary representing a MESHInboxMessageDownloaded event"""
7+
return {
8+
"id": event_id,
9+
"specversion": "1.0",
10+
"source": (
11+
"/nhs/england/notify/production/primary/"
12+
'data-plane/digitalletters/mesh'
13+
),
14+
"subject": (
15+
'customer/920fca11-596a-4eca-9c47-99f624614658/'
16+
'recipient/769acdd4-6a47-496f-999f-76a6fd2c3959'
17+
),
18+
"type": (
19+
'uk.nhs.notify.digital.letters.mesh.inbox.message.downloaded.v1'
20+
),
21+
"time": '2026-01-08T10:00:00Z',
22+
"recordedtime": '2026-01-08T10:00:00Z',
23+
"severitynumber": 2,
24+
"severitytext": 'INFO',
25+
"traceparent": '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
26+
"datacontenttype": 'application/json',
27+
"dataschema": (
28+
'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10-draft/data/'
29+
'digital-letters-mesh-inbox-message-downloaded-data.schema.json'
30+
),
31+
"datacategory": "non-sensitive",
32+
"dataclassification": "public",
33+
"dataregulation": "GDPR",
34+
"data": {
35+
"meshMessageId": "MSG123456",
36+
"messageUri": f"https://example.com/ttl/resource/{event_id}",
37+
"messageReference": "REF123",
38+
"senderId": "SENDER001",
39+
}
40+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"""
2+
Tests for MeshAcknowledger class
3+
"""
4+
import json
5+
from unittest.mock import Mock
6+
import pytest
7+
from mesh_acknowledge.acknowledger import (
8+
MeshAcknowledger,
9+
NOTIFY_ACK_WORKFLOW_ID,
10+
ACK_SUBJECT
11+
)
12+
13+
SENT_MESH_MESSAGE_ID = "MSG123456"
14+
15+
16+
@pytest.fixture(name='mock_mesh_client')
17+
def create_mock_mesh_client():
18+
"""Create a mock MeshClient for testing"""
19+
client = Mock()
20+
client.handshake = Mock()
21+
client.send_message = Mock(return_value=SENT_MESH_MESSAGE_ID)
22+
return client
23+
24+
25+
@pytest.fixture(name='mock_logger')
26+
def create_mock_logger():
27+
"""Create a mock logger for testing"""
28+
logger = Mock()
29+
logger.debug = Mock()
30+
return logger
31+
32+
33+
@pytest.fixture(name='acknowledger')
34+
def create_acknowledger(mock_mesh_client, mock_logger):
35+
"""Create a MeshAcknowledger instance with mocked dependencies"""
36+
return MeshAcknowledger(mock_mesh_client, mock_logger)
37+
38+
39+
class TestMeshAcknowledger:
40+
"""Test suite for MeshAcknowledger class"""
41+
42+
def test_init_performs_handshake(self, mock_mesh_client, mock_logger):
43+
"""Test that __init__ performs a MESH handshake"""
44+
MeshAcknowledger(mock_mesh_client, mock_logger)
45+
46+
mock_mesh_client.handshake.assert_called_once()
47+
48+
def test_acknowledge_message_sends_correct_message(
49+
self, acknowledger, mock_mesh_client
50+
):
51+
"""Test that acknowledge_message sends the correct message via MESH"""
52+
mailbox_id = "MAILBOX001"
53+
message_id = "MSG123456"
54+
message_reference = "REF789"
55+
sender_id = "SENDER001"
56+
57+
expected_body = json.dumps({
58+
"meshMessageId": message_id,
59+
"requestId": f"{sender_id}_{message_reference}"
60+
}).encode()
61+
62+
acknowledger.acknowledge_message(
63+
mailbox_id, message_id, message_reference, sender_id
64+
)
65+
66+
mock_mesh_client.send_message.assert_called_once_with(
67+
mailbox_id,
68+
expected_body,
69+
workflow_id=NOTIFY_ACK_WORKFLOW_ID,
70+
local_id=message_reference,
71+
subject=ACK_SUBJECT
72+
)
73+
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+
93+
def test_acknowledge_message_raises_error_if_mesh_send_fails(
94+
self, acknowledger, mock_mesh_client
95+
):
96+
"""Test that the MeshAcknowledger raises an error if MESH send_message fails"""
97+
mailbox_id = "MAILBOX001"
98+
message_id = "MSG123"
99+
message_reference = "REF123"
100+
sender_id = "SENDER001"
101+
expected_exception_message = "MESH send failed"
102+
103+
mock_mesh_client.send_message.side_effect = Exception(
104+
expected_exception_message)
105+
106+
with pytest.raises(Exception, match=expected_exception_message):
107+
acknowledger.acknowledge_message(
108+
mailbox_id, message_id, message_reference, sender_id
109+
)
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"""
2+
Tests for Dlq class in mesh_acknowledge.dlq
3+
"""
4+
import json
5+
from unittest.mock import Mock
6+
7+
import pytest
8+
from botocore.exceptions import ClientError
9+
10+
from mesh_acknowledge.dlq import Dlq
11+
12+
13+
@pytest.fixture(name='mock_sqs_client')
14+
def create_mock_sqs_client():
15+
"""Create a mock SQS client for testing"""
16+
client = Mock()
17+
client.send_message = Mock(return_value={'MessageId': 'msg-12345'})
18+
return client
19+
20+
21+
@pytest.fixture(name='mock_logger')
22+
def create_mock_logger():
23+
"""Create a mock logger for testing"""
24+
logger = Mock()
25+
logger.info = Mock()
26+
logger.error = Mock()
27+
return logger
28+
29+
@pytest.fixture(name='dlq_url')
30+
def create_dlq_url():
31+
"""Create a DLQ URL for testing"""
32+
return "https://sqs.us-east-1.amazonaws.com/123456789012/test-dlq"
33+
34+
@pytest.fixture(name='dlq')
35+
def create_dlq(mock_sqs_client, mock_logger, dlq_url):
36+
"""Create a Dlq instance for testing"""
37+
return Dlq(
38+
sqs_client=mock_sqs_client,
39+
dlq_url=dlq_url,
40+
logger=mock_logger
41+
)
42+
43+
44+
class TestSendToQueue:
45+
"""Tests for send_to_queue method"""
46+
47+
def test_sends_record_to_dlq_successfully(
48+
self,
49+
dlq,
50+
mock_sqs_client,
51+
dlq_url
52+
):
53+
"""Test that a record is sent to DLQ successfully"""
54+
record = {
55+
"id": "test-event-123",
56+
"type": "test.event.v1",
57+
"data": {"key": "value"}
58+
}
59+
reason = "Validation failed"
60+
61+
dlq.send_to_queue(record, reason)
62+
63+
mock_sqs_client.send_message.assert_called_once_with(
64+
QueueUrl=dlq_url,
65+
MessageBody=json.dumps(record),
66+
MessageAttributes={
67+
'DlqReason': {
68+
'DataType': 'String',
69+
'StringValue': reason
70+
}
71+
}
72+
)
73+
74+
def test_handles_sqs_client_error(
75+
self,
76+
dlq,
77+
mock_sqs_client,
78+
):
79+
"""Test that ClientError from SQS is handled and re-raised"""
80+
record = {"id": "test-event-123"}
81+
reason = "Processing error"
82+
error = ClientError(
83+
{'Error': {'Code': 'InvalidParameterValue', 'Message': 'Invalid queue URL'}},
84+
'SendMessage'
85+
)
86+
mock_sqs_client.send_message.side_effect = error
87+
88+
with pytest.raises(ClientError):
89+
dlq.send_to_queue(record, reason)

0 commit comments

Comments
 (0)