Skip to content

Commit 7691746

Browse files
committed
CCM-12616: resolve comments
1 parent 9a401da commit 7691746

File tree

11 files changed

+295
-134
lines changed

11 files changed

+295
-134
lines changed

.coverage

-52 KB
Binary file not shown.

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

Lines changed: 68 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,11 @@ def create_sqs_event(num_records=1, event_source='aws:sqs'):
4242
class TestHandler:
4343
"""Test suite for Lambda handler"""
4444

45+
@patch('src.handler.EventPublisher')
46+
@patch('src.handler.DocumentStore')
4547
@patch('src.handler.Config')
4648
@patch('src.handler.MeshDownloadProcessor')
47-
def test_handler_success_single_message(self, mock_processor_class, mock_config_class):
49+
def test_handler_success_single_message(self, mock_processor_class, mock_config_class, mock_doc_store_class, mock_event_pub_class):
4850
"""Test successful handler execution with single SQS message"""
4951
from src.handler import handler
5052

@@ -54,6 +56,11 @@ def test_handler_success_single_message(self, mock_processor_class, mock_config_
5456
mock_config_class.return_value.__exit__ = Mock(return_value=None)
5557
mock_processor_class.return_value = mock_processor
5658

59+
mock_doc_store = Mock()
60+
mock_doc_store_class.return_value = mock_doc_store
61+
mock_event_pub = Mock()
62+
mock_event_pub_class.return_value = mock_event_pub
63+
5764
event = create_sqs_event(num_records=1)
5865

5966
result = handler(event, mock_context)
@@ -71,9 +78,11 @@ def test_handler_success_single_message(self, mock_processor_class, mock_config_
7178

7279
assert result == {"batchItemFailures": []}
7380

81+
@patch('src.handler.EventPublisher')
82+
@patch('src.handler.DocumentStore')
7483
@patch('src.handler.Config')
7584
@patch('src.handler.MeshDownloadProcessor')
76-
def test_handler_success_multiple_messages(self, mock_processor_class, mock_config_class):
85+
def test_handler_success_multiple_messages(self, mock_processor_class, mock_config_class, mock_doc_store_class, mock_event_pub_class):
7786
"""Test successful handler execution with multiple SQS messages"""
7887
from src.handler import handler
7988

@@ -83,6 +92,9 @@ def test_handler_success_multiple_messages(self, mock_processor_class, mock_conf
8392
mock_config_class.return_value.__exit__ = Mock(return_value=None)
8493
mock_processor_class.return_value = mock_processor
8594

95+
mock_doc_store_class.return_value = Mock()
96+
mock_event_pub_class.return_value = Mock()
97+
8698
event = create_sqs_event(num_records=3)
8799

88100
result = handler(event, mock_context)
@@ -93,9 +105,11 @@ def test_handler_success_multiple_messages(self, mock_processor_class, mock_conf
93105
# Verify return value (no failures)
94106
assert result == {"batchItemFailures": []}
95107

108+
@patch('src.handler.EventPublisher')
109+
@patch('src.handler.DocumentStore')
96110
@patch('src.handler.Config')
97111
@patch('src.handler.MeshDownloadProcessor')
98-
def test_handler_config_cleanup_on_success(self, mock_processor_class, mock_config_class):
112+
def test_handler_config_cleanup_on_success(self, mock_processor_class, mock_config_class, mock_doc_store_class, mock_event_pub_class):
99113
"""Test that Config context manager cleanup is called on success"""
100114
from src.handler import handler
101115

@@ -106,16 +120,21 @@ def test_handler_config_cleanup_on_success(self, mock_processor_class, mock_conf
106120
mock_config_class.return_value.__exit__ = mock_exit
107121
mock_processor_class.return_value = mock_processor
108122

123+
mock_doc_store_class.return_value = Mock()
124+
mock_event_pub_class.return_value = Mock()
125+
109126
event = create_sqs_event(num_records=1)
110127

111128
handler(event, mock_context)
112129

113130
mock_exit.assert_called_once()
114131
assert mock_exit.call_args[0] == (None, None, None)
115132

133+
@patch('src.handler.EventPublisher')
134+
@patch('src.handler.DocumentStore')
116135
@patch('src.handler.Config')
117136
@patch('src.handler.MeshDownloadProcessor')
118-
def test_handler_partial_batch_failure(self, mock_processor_class, mock_config_class):
137+
def test_handler_partial_batch_failure(self, mock_processor_class, mock_config_class, mock_doc_store_class, mock_event_pub_class):
119138
"""Test handler handles partial batch failures correctly"""
120139
from src.handler import handler
121140

@@ -125,6 +144,9 @@ def test_handler_partial_batch_failure(self, mock_processor_class, mock_config_c
125144
mock_config_class.return_value.__exit__ = Mock(return_value=None)
126145
mock_processor_class.return_value = mock_processor
127146

147+
mock_doc_store_class.return_value = Mock()
148+
mock_event_pub_class.return_value = Mock()
149+
128150
# Make second message fail
129151
mock_processor.process_sqs_message.side_effect = [
130152
None,
@@ -140,9 +162,11 @@ def test_handler_partial_batch_failure(self, mock_processor_class, mock_config_c
140162
assert len(result["batchItemFailures"]) == 1
141163
assert result["batchItemFailures"][0]["itemIdentifier"] == "msg-1"
142164

165+
@patch('src.handler.EventPublisher')
166+
@patch('src.handler.DocumentStore')
143167
@patch('src.handler.Config')
144168
@patch('src.handler.MeshDownloadProcessor')
145-
def test_handler_skips_non_sqs_records(self, mock_processor_class, mock_config_class):
169+
def test_handler_skips_non_sqs_records(self, mock_processor_class, mock_config_class, mock_doc_store_class, mock_event_pub_class):
146170
"""Test handler skips records that are not from SQS"""
147171
from src.handler import handler
148172

@@ -152,6 +176,9 @@ def test_handler_skips_non_sqs_records(self, mock_processor_class, mock_config_c
152176
mock_config_class.return_value.__exit__ = Mock(return_value=None)
153177
mock_processor_class.return_value = mock_processor
154178

179+
mock_doc_store_class.return_value = Mock()
180+
mock_event_pub_class.return_value = Mock()
181+
155182
event = create_sqs_event(num_records=1, event_source='aws:dynamodb')
156183

157184
result = handler(event, mock_context)
@@ -160,29 +187,41 @@ def test_handler_skips_non_sqs_records(self, mock_processor_class, mock_config_c
160187

161188
assert result == {"batchItemFailures": []}
162189

190+
@patch('src.handler.EventPublisher')
191+
@patch('src.handler.DocumentStore')
163192
@patch('src.handler.Config')
164193
@patch('src.handler.MeshDownloadProcessor')
165-
def test_handler_config_cleanup_on_exception(self, mock_processor_class, mock_config_class):
194+
def test_handler_config_cleanup_on_exception(self, mock_processor_class, mock_config_class, mock_doc_store_class, mock_event_pub_class):
166195
"""Test that Config context manager cleanup is called even on exception"""
167196
from src.handler import handler
168197

169198
(mock_context, mock_config, mock_processor) = setup_mocks()
170199

171-
# Make config.__enter__ raise an exception
172-
test_exception = RuntimeError("Config error")
173-
mock_config_class.return_value.__enter__.side_effect = test_exception
200+
mock_config_class.return_value.__enter__.return_value = mock_config
174201
mock_exit = Mock(return_value=None)
175202
mock_config_class.return_value.__exit__ = mock_exit
176203

204+
mock_doc_store_class.return_value = Mock()
205+
mock_event_pub_class.return_value = Mock()
206+
207+
test_exception = RuntimeError("Processing error")
208+
mock_processor.process_sqs_message.side_effect = test_exception
209+
mock_processor_class.return_value = mock_processor
210+
177211
event = create_sqs_event(num_records=1)
178212

179-
# Handler should raise the exception
180-
with pytest.raises(RuntimeError, match="Config error"):
181-
handler(event, mock_context)
213+
result = handler(event, mock_context)
214+
215+
mock_exit.assert_called_once()
216+
217+
# Verify the failed message is in batch failures
218+
assert len(result["batchItemFailures"]) == 1
182219

220+
@patch('src.handler.EventPublisher')
221+
@patch('src.handler.DocumentStore')
183222
@patch('src.handler.Config')
184223
@patch('src.handler.MeshDownloadProcessor')
185-
def test_handler_returns_empty_failures_on_empty_event(self, mock_processor_class, mock_config_class):
224+
def test_handler_returns_empty_failures_on_empty_event(self, mock_processor_class, mock_config_class, mock_doc_store_class, mock_event_pub_class):
186225
"""Test handler handles empty event gracefully"""
187226
from src.handler import handler
188227

@@ -192,6 +231,9 @@ def test_handler_returns_empty_failures_on_empty_event(self, mock_processor_clas
192231
mock_config_class.return_value.__exit__ = Mock(return_value=None)
193232
mock_processor_class.return_value = mock_processor
194233

234+
mock_doc_store_class.return_value = Mock()
235+
mock_event_pub_class.return_value = Mock()
236+
195237
event = {'Records': []}
196238

197239
result = handler(event, mock_context)
@@ -200,9 +242,11 @@ def test_handler_returns_empty_failures_on_empty_event(self, mock_processor_clas
200242

201243
assert result == {"batchItemFailures": []}
202244

245+
@patch('src.handler.EventPublisher')
246+
@patch('src.handler.DocumentStore')
203247
@patch('src.handler.Config')
204248
@patch('src.handler.MeshDownloadProcessor')
205-
def test_handler_passes_correct_parameters_to_processor(self, mock_processor_class, mock_config_class):
249+
def test_handler_passes_correct_parameters_to_processor(self, mock_processor_class, mock_config_class, mock_doc_store_class, mock_event_pub_class):
206250
"""Test that handler passes all required parameters to MeshDownloadProcessor"""
207251
from src.handler import handler
208252

@@ -217,13 +261,22 @@ def test_handler_passes_correct_parameters_to_processor(self, mock_processor_cla
217261
mock_config_class.return_value.__exit__ = Mock(return_value=None)
218262
mock_processor_class.return_value = mock_processor
219263

264+
mock_doc_store = Mock()
265+
mock_doc_store_class.return_value = mock_doc_store
266+
mock_event_pub = Mock()
267+
mock_event_pub_class.return_value = mock_event_pub
268+
220269
event = create_sqs_event(num_records=1)
221270

222271
handler(event, mock_context)
223272

224273
mock_processor_class.assert_called_once()
225274
call_kwargs = mock_processor_class.call_args[1]
226275

227-
# Handler now passes the entire config object
276+
# Handler now passes the entire config object and dependencies
228277
assert call_kwargs['config'] == mock_config
278+
assert call_kwargs['mesh_client'] == mock_mesh_client
279+
assert call_kwargs['download_metric'] == mock_download_metric
280+
assert call_kwargs['document_store'] == mock_doc_store
281+
assert call_kwargs['event_publisher'] == mock_event_pub
229282
assert 'log' in call_kwargs

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

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,15 +90,17 @@ def create_mesh_message(message_id='test_123', sender='SENDER_001', local_id='re
9090
class TestMeshDownloadProcessor:
9191
"""Test suite for MeshDownloadProcessor"""
9292

93-
def test_processor_initialization_with_event_publisher(self):
94-
"""Processor initializes and handshakes mesh client when EventPublisher configured"""
93+
def test_processor_initialization_calls_mesh_handshake(self):
94+
"""Processor initializes and handshakes mesh client"""
9595
from src.processor import MeshDownloadProcessor
9696

9797
config, log, event_publisher, document_store = setup_mocks()
9898

9999
processor = MeshDownloadProcessor(
100100
config=config,
101101
log=log,
102+
mesh_client=config.mesh_client,
103+
download_metric=config.download_metric,
102104
document_store=document_store,
103105
event_publisher=event_publisher
104106
)
@@ -122,6 +124,8 @@ def test_process_sqs_message_success(self, mock_datetime):
122124
processor = MeshDownloadProcessor(
123125
config=config,
124126
log=log,
127+
mesh_client=config.mesh_client,
128+
download_metric=config.download_metric,
125129
document_store=document_store,
126130
event_publisher=event_publisher
127131
)
@@ -149,6 +153,26 @@ def test_process_sqs_message_success(self, mock_datetime):
149153

150154
event_publisher.send_events.assert_called_once()
151155

156+
# Verify the published event content
157+
published_events = event_publisher.send_events.call_args[0][0]
158+
assert len(published_events) == 1
159+
160+
published_event = published_events[0]
161+
162+
# Verify CloudEvent envelope fields
163+
assert published_event['type'] == 'uk.nhs.notify.digital.letters.mesh.inbox.message.downloaded.v1'
164+
assert published_event['source'] == '/nhs/england/notify/development/primary/data-plane/digitalletters/mesh'
165+
assert published_event['subject'] == 'customer/00000000-0000-0000-0000-000000000000/recipient/00000000-0000-0000-0000-000000000000'
166+
assert published_event['time'] == '2025-11-19T15:30:45+00:00'
167+
assert 'id' in published_event
168+
169+
# Verify CloudEvent data payload
170+
event_data = published_event['data']
171+
assert event_data['senderId'] == 'TEST_SENDER'
172+
assert event_data['messageReference'] == 'ref_001'
173+
assert event_data['messageUri'] == 's3://test-pii-bucket/document-reference/SENDER_001_ref_001'
174+
assert set(event_data.keys()) == {'senderId', 'messageReference', 'messageUri'}
175+
152176
def test_process_sqs_message_validation_failure(self):
153177
"""Malformed CloudEvents should be rejected by pydantic and not trigger downloads"""
154178
from src.processor import MeshDownloadProcessor
@@ -158,6 +182,8 @@ def test_process_sqs_message_validation_failure(self):
158182
processor = MeshDownloadProcessor(
159183
config=config,
160184
log=log,
185+
mesh_client=config.mesh_client,
186+
download_metric=config.download_metric,
161187
document_store=document_store,
162188
event_publisher=event_publisher
163189
)
@@ -180,6 +206,8 @@ def test_process_sqs_message_missing_mesh_message_id(self):
180206
processor = MeshDownloadProcessor(
181207
config=config,
182208
log=log,
209+
mesh_client=config.mesh_client,
210+
download_metric=config.download_metric,
183211
document_store=document_store,
184212
event_publisher=event_publisher
185213
)
@@ -203,6 +231,8 @@ def test_download_and_store_message_not_found(self):
203231
processor = MeshDownloadProcessor(
204232
config=config,
205233
log=log,
234+
mesh_client=config.mesh_client,
235+
download_metric=config.download_metric,
206236
document_store=document_store,
207237
event_publisher=event_publisher
208238
)
@@ -225,6 +255,8 @@ def test_document_store_failure_prevents_ack_and_raises(self):
225255
processor = MeshDownloadProcessor(
226256
config=config,
227257
log=log,
258+
mesh_client=config.mesh_client,
259+
download_metric=config.download_metric,
228260
document_store=document_store,
229261
event_publisher=event_publisher
230262
)
@@ -258,6 +290,8 @@ def test_bucket_selection_with_mesh_mock_enabled(self, mock_datetime):
258290
processor = MeshDownloadProcessor(
259291
config=config,
260292
log=log,
293+
mesh_client=config.mesh_client,
294+
download_metric=config.download_metric,
261295
document_store=document_store,
262296
event_publisher=event_publisher
263297
)
@@ -294,6 +328,8 @@ def test_bucket_selection_with_mesh_mock_disabled(self, mock_datetime):
294328
processor = MeshDownloadProcessor(
295329
config=config,
296330
log=log,
331+
mesh_client=config.mesh_client,
332+
download_metric=config.download_metric,
297333
document_store=document_store,
298334
event_publisher=event_publisher
299335
)
@@ -319,6 +355,8 @@ def test_message_reference_mismatch_raises_error(self):
319355
processor = MeshDownloadProcessor(
320356
config=config,
321357
log=log,
358+
mesh_client=config.mesh_client,
359+
download_metric=config.download_metric,
322360
document_store=document_store,
323361
event_publisher=event_publisher
324362
)

lambdas/mesh-download/src/document_store.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ class IntermediaryBodyStoreError(Exception):
55
"""Error to represent any failure to upload document to intermediate location"""
66

77

8+
class DocumentStoreConfig:
9+
"""Configuration holder for DocumentStore"""
10+
def __init__(self, s3_client, transactional_data_bucket):
11+
self.s3_client = s3_client
12+
self.transactional_data_bucket = transactional_data_bucket
13+
14+
815
class DocumentStore: # pylint: disable=too-few-public-methods
916
"""Class for storing document references in S3"""
1017

lambdas/mesh-download/src/handler.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
"""lambda handler for mesh download"""
22

33
import json
4+
from event_publisher import EventPublisher
5+
46
from .config import Config, log
57
from .processor import MeshDownloadProcessor
8+
from .document_store import DocumentStore, DocumentStoreConfig
69

710

811
def handler(event, context):
@@ -23,9 +26,25 @@ def handler(event, context):
2326

2427
try:
2528
with Config() as config:
29+
doc_store_config = DocumentStoreConfig(
30+
s3_client=config.s3_client,
31+
transactional_data_bucket=config.transactional_data_bucket
32+
)
33+
document_store = DocumentStore(doc_store_config)
34+
35+
event_publisher = EventPublisher(
36+
event_bus_arn=config.event_publisher_event_bus_arn,
37+
dlq_url=config.event_publisher_dlq_url,
38+
logger=log
39+
)
40+
2641
processor = MeshDownloadProcessor(
2742
config=config,
28-
log=log
43+
log=log,
44+
mesh_client=config.mesh_client,
45+
download_metric=config.download_metric,
46+
document_store=document_store,
47+
event_publisher=event_publisher
2948
)
3049

3150
# Process each SQS record

0 commit comments

Comments
 (0)