"""Unit tests for the handler function.""" from unittest.mock import Mock import pytest import config # noqa from src.app import _handle_failed_send from src.app import _handle_successful_send from src.app import _process_triggered_send from src.app import delete_s3_file from src.app import handler from src.app import process_triggered_sends from src.app import read_s3_file from src.kafka_producer import TriggeredSendEventProducer from src.models import TriggeredSend class TestHandler: """Unit tests for the handler function in the app module.""" @pytest.fixture def mock_delete_s3_file(self, mocker): """Mock delete_s3_file function.""" return mocker.patch('src.app.delete_s3_file') @pytest.fixture def s3_event(self): """Sample S3 event for testing.""" return { 'Records': [ { 's3': { 'bucket': {'name': 'test-bucket'}, 'object': {'key': 'test-key'} } } ] } def test_handler(self, mocker, s3_event, mock_delete_s3_file): """Test the handler function.""" mock_read_s3_file = mocker.patch('src.app.read_s3_file', return_value=['record1', 'record2']) mock_process_triggered_sends = mocker.patch('src.app.process_triggered_sends') response = handler(s3_event, {}) mock_read_s3_file.assert_called_once_with('test-bucket', 'test-key') mock_process_triggered_sends.assert_called_once() mock_delete_s3_file.assert_called_once_with('test-bucket', 'test-key') assert response['statusCode'] == 200 assert 'processed successfully' in response['body'] def test_handler_with_exception(self, mocker, s3_event, mock_delete_s3_file): """Test the handler function when an exception is raised during execution.""" mock_read_s3_file = mocker.patch('src.app.read_s3_file', side_effect=Exception('Test exception')) response = handler(s3_event, {}) mock_read_s3_file.assert_called_once_with('test-bucket', 'test-key') mock_delete_s3_file.assert_called_once_with('test-bucket', 'test-key') assert response['statusCode'] == 200 assert 'failed to process with error' in response['body'] def test_handler_missing_records_key(self, mocker): """Test handler returns 400 if 'Records' key is missing.""" event_no_records = {} response = handler(event_no_records, None) assert response['statusCode'] == 400 assert 'Records' in response['body'] or 'does not contain' in response['body'] def test_handler_empty_records(self, mocker): """Test handler returns 400 if 'Records' is empty.""" event_empty_records = {'Records': []} response = handler(event_empty_records, None) assert response['statusCode'] == 400 assert 'Records' in response['body'] or 'does not contain' in response['body'] @pytest.fixture def triggered_send_event_producer(mocker): """Mock TriggeredSendEventProducer instance.""" return mocker.patch('src.app.TriggeredSendEventProducer') class TestProcessTriggeredSends: """Unit tests for the process_triggered_sends function.""" @pytest.fixture def triggered_send_info_records(self): """Sample triggered send info records.""" return ['record1', 'record2'] @pytest.fixture def mock_process_triggered_send(self, mocker): """Mock _process_triggered_send function.""" return mocker.patch('src.app._process_triggered_send') @pytest.fixture def mock_model_validate_json(self, mocker, triggered_send_info_records): """Mock model_validate_json method.""" validated_records = [f'validated_{record}' for record in triggered_send_info_records] return mocker.patch('src.models.TriggeredSend.model_validate_json', side_effect=validated_records) def test_process_triggered_sends( self, mocker, mock_process_triggered_send, mock_model_validate_json, triggered_send_info_records, triggered_send_event_producer): """Test process_triggered_sends function.""" process_triggered_sends(iter(triggered_send_info_records)) assert mock_process_triggered_send.call_count == len(triggered_send_info_records) assert mock_model_validate_json.call_count == len(triggered_send_info_records) mock_process_triggered_send.assert_has_calls([ mocker.call(validated_record, triggered_send_event_producer.return_value) for validated_record in mock_model_validate_json.side_effect ]) class TestProcessTriggeredSend: """Unit tests for the _process_triggered_send function.""" @pytest.fixture def triggered_send_info(self): """Fixture for TriggeredSend mock object.""" return Mock(spec=TriggeredSend) @pytest.fixture def mock_triggered_send_response_validation(self, mocker): """Mock triggered_send_response_validation function.""" return mocker.patch( 'src.marketing_cloud_api.triggered_sends.triggered_send_response_validation', return_value=('mock_response_payload', None)) @pytest.fixture def mock_submit_triggered_send(self, mocker): """Mock submit_triggered_send function.""" return mocker.patch( 'src.marketing_cloud_api.triggered_sends.submit_triggered_send', return_value=('response', None)) @pytest.fixture def mock_handle_successful_send(self, mocker): """Mock _handle_successful_send function.""" return mocker.patch('src.app._handle_successful_send') @pytest.fixture def mock_handle_failed_send(self, mocker): """Mock _handle_failed_send function.""" return mocker.patch('src.app._handle_failed_send') def test_process_triggered_send( self, triggered_send_info, mock_triggered_send_response_validation, mock_submit_triggered_send, mock_handle_successful_send, triggered_send_event_producer): """Test _process_triggered_send function.""" _process_triggered_send(triggered_send_info, triggered_send_event_producer) mock_submit_triggered_send.assert_called_once_with(triggered_send_info) mock_triggered_send_response_validation.assert_called_once_with('response') mock_handle_successful_send.assert_called_once_with(triggered_send_info, triggered_send_event_producer) def test_process_triggered_send_with_submit_error( self, triggered_send_info, mock_submit_triggered_send, mock_handle_failed_send, triggered_send_event_producer): """Test _process_triggered_send with submit error.""" mock_submit_triggered_send.return_value = (None, 'submit error') _process_triggered_send(triggered_send_info, triggered_send_event_producer) mock_submit_triggered_send.assert_called_once_with(triggered_send_info) mock_handle_failed_send.assert_called_once_with( triggered_send_info, 'submit error', triggered_send_event_producer) def test_process_triggered_send_with_validation_error( self, triggered_send_info, mock_triggered_send_response_validation, mock_submit_triggered_send, mock_handle_failed_send, triggered_send_event_producer): """Test _process_triggered_send with validation error.""" mock_triggered_send_response_validation.return_value = ('mock_response_payload', 'validation error') _process_triggered_send(triggered_send_info, triggered_send_event_producer) mock_submit_triggered_send.assert_called_once_with(triggered_send_info) mock_triggered_send_response_validation.assert_called_once_with('response') mock_handle_failed_send.assert_called_once_with( triggered_send_info, 'validation error', triggered_send_event_producer, 'mock_response_payload') class TestHandleFailedSend: """Unit tests for the _handle_failed_send function.""" def test_failed_send(self, mocker): """Test _handle_failed_send function.""" triggered_send_info = Mock(spec=TriggeredSend) kafka_producer = Mock(spec=TriggeredSendEventProducer) error = 'Test error' api_response = { 'status': 'failed', 'requestId': 'test-request-id', 'batchHasErrors': False, 'responses': [ { 'recipientSendId': '12345', 'hasErrors': True, 'messages': ['Error message'] } ] } mock_produce_failed_event = mocker.patch.object(kafka_producer, 'produce_failed_event') _handle_failed_send(triggered_send_info, error, kafka_producer, api_response) mock_produce_failed_event.assert_called_once() class TestHandleSuccessfulSend: """Unit tests for the _handle_successful_send function.""" def test_successful_send(self, mocker): """Test _handle_successful_send function.""" triggered_send_info = Mock(spec=TriggeredSend) kafka_producer = Mock(spec=TriggeredSendEventProducer) mock_produce_successful_event = mocker.patch.object(kafka_producer, 'produce_successful_event') _handle_successful_send(triggered_send_info, kafka_producer) mock_produce_successful_event.assert_called_once() class TestReadS3File: """Unit tests for the read_s3_file function.""" def test_read_s3_file(self, mocker): """Test read_s3_file function.""" bucket_name = 'test-bucket' object_key = 'test-key' mock_s3_client = mocker.patch('boto3.client') mock_s3_client.return_value.get_object.return_value = { 'Body': Mock(iter_lines=lambda: [b'line1', b'line2']) } lines = list(read_s3_file(bucket_name, object_key)) assert lines == ['line1', 'line2'] class TestDeleteS3File: """Unit tests for the delete_s3_file function.""" def test_delete_s3_file(self, mocker): """Test delete_s3_file function.""" bucket_name = 'test-bucket' object_key = 'test-key' mock_s3_client = mocker.patch('boto3.client') delete_s3_file(bucket_name, object_key) mock_s3_client.return_value.delete_object.assert_called_once_with( Bucket=bucket_name, Key=object_key, ExpectedBucketOwner=config.AWS_ACCOUNT_ID )