"""Lambda test module.""" from contextlib import nullcontext as does_not_raise from unittest.mock import call from unittest.mock import MagicMock from unittest.mock import NonCallableMagicMock from content_utils.exceptions import IneligibleEventError import pytest from src.constants import ADD_INDEX_OP from src.logic import event_handling from src.logic import step_function def test_handler(mock_cdc_event, mock_indexing_data_inputs, mock_sfn_execution_arn, mocker): """Test handler function.""" from src import app from src.logic import feature mocker.patch.object( feature, 'is_store_product_enabled', return_value=False, ) mocker.patch.object( app.step_function, 'trigger_sfn', return_value=mock_sfn_execution_arn, ) result = app.handler(mock_cdc_event, None) # After line 60 in app.py, add_inputs are extended to other_indexing_inputs # So order is: deindex items first, then add_to_index items expected_inputs = [ i for i in mock_indexing_data_inputs if i['operation_type'] != ADD_INDEX_OP ] + [ i for i in mock_indexing_data_inputs if i['operation_type'] == ADD_INDEX_OP ] step_function.trigger_sfn.assert_called_with({ 'data': { 'indexing_inputs': expected_inputs } }) assert result == { 'status': 'OK' } debezium_debezium_message_response = MagicMock() app_content_lambda_logger = NonCallableMagicMock(set_data=MagicMock()) @pytest.mark.parametrize(( 'test_description', 'msk_message', 'expected_debezium_message_calls', 'expected_set_data_calls', 'expected_parse_message_payload_calls', 'expected_parse_message_payload_side_effect', 'expected_raise', 'expected_raise_message', ), [ ( 'no message body', NonCallableMagicMock(value=None), [], [call(status='skip', result='no_message_body')], [], None, does_not_raise(), 'None', ), ( 'cdc message', NonCallableMagicMock( topic='cdc.contentReview.reviewQueue', value='cdc_value', ), [call('cdc_value', 'cdc.contentReview.reviewQueue', allowed_event_ops=['c', 'u', 'd'])], [call(status='success')], [call(debezium_debezium_message_response)], None, does_not_raise(), 'None', ), ( 'unsupported topic', NonCallableMagicMock( topic='unsupported', value='whatever', ), [], [call(status='error', result='Topic unsupported is not supported.')], [], None, pytest.raises(Exception), 'Topic unsupported is not supported.', ), ( 'parse_message_payload IneligibleEventError', NonCallableMagicMock( topic='cdc.contentReview.reviewQueue', value='cdc_value', ), [call('cdc_value', 'cdc.contentReview.reviewQueue', allowed_event_ops=['c', 'u', 'd'])], [call(status='skip', result='IneligibleEventError message.')], [call(debezium_debezium_message_response)], IneligibleEventError('IneligibleEventError message.'), does_not_raise(), 'None', ), ( 'parse_message_payload Exception', NonCallableMagicMock( topic='cdc.contentReview.reviewQueue', value='cdc_value', ), [call('cdc_value', 'cdc.contentReview.reviewQueue', allowed_event_ops=['c', 'u', 'd'])], [call(status='error', result='some exception')], [call(debezium_debezium_message_response)], Exception('some exception'), pytest.raises(Exception), 'some exception', ) ]) def test_process_event( test_description, msk_message, expected_debezium_message_calls, expected_set_data_calls, expected_parse_message_payload_calls, expected_parse_message_payload_side_effect, expected_raise, expected_raise_message, mocker, ): """Test process_event success.""" from src import app from kafka_utils.consumer.message import debezium mocker.patch.object( app, 'content_lambda_logger', new=app_content_lambda_logger, ) mocker.patch.object( debezium, 'DebeziumMessage', return_value=debezium_debezium_message_response ) mocker.patch.object( event_handling, 'parse_message_payload', side_effect=expected_parse_message_payload_side_effect, ) with expected_raise as er: app.process_event(msk_message) assert str(getattr(er, 'value', None)) == expected_raise_message assert debezium.DebeziumMessage.mock_calls == ( expected_debezium_message_calls) assert app.content_lambda_logger.set_data.mock_calls == ( expected_set_data_calls) assert event_handling.parse_message_payload.mock_calls == ( expected_parse_message_payload_calls) app_content_lambda_logger.reset_mock() def test_app_handler_errors(mock_cdc_event, mocker): """Test app.handler errors.""" from src import app from src.logic import feature mocker.patch.object(feature, 'is_store_product_enabled', return_value=False) mock_handler = mocker.patch('src.app.event_handling') mock_log = mocker.patch('src.app.content_lambda_logger') mock_handler.parse_message_payload.side_effect = KeyError('foo') with pytest.raises(KeyError): app.handler(mock_cdc_event, None) mock_log.set_data.assert_called_with(status='error', result="'foo'") def test_empty_msk_messages(mock_empty_event, mocker): """Test warning on empty msk messages.""" from src import app from src.logic import feature mocker.patch.object(feature, 'is_store_product_enabled', return_value=False) mocker.patch('src.app.event_handling') mock_log = mocker.patch('src.app.content_lambda_logger') app.handler(mock_empty_event, None) mock_log.set_data.assert_called_with(status='skip', result='no_message_body') def test_handler_exception(mock_cdc_event, mock_indexing_data_inputs, mocker,): """Test handler function.""" from src import app from src.logic import feature mocker.patch.object( feature, 'is_store_product_enabled', return_value=False, ) mocker.patch.object( app.step_function, 'trigger_sfn', side_effect=Exception('Mocked exception'), ) with pytest.raises(Exception) as er: app.handler(mock_cdc_event, None) assert str(er.value) == 'Mocked exception' # After line 60 in app.py, add_inputs are extended to other_indexing_inputs # So order is: deindex items first, then add_to_index items expected_inputs = [ i for i in mock_indexing_data_inputs if i['operation_type'] != ADD_INDEX_OP ] + [ i for i in mock_indexing_data_inputs if i['operation_type'] == ADD_INDEX_OP ] step_function.trigger_sfn.assert_called_with({ 'data': { 'indexing_inputs': expected_inputs } }) def test_boto3_start_execution_exception( mock_cdc_event, mock_indexing_data_inputs, mocker ): """Test boto3 start_execution throwing an exception.""" from src import app from src.logic import feature mocker.patch.object( feature, 'is_store_product_enabled', return_value=False, ) mock_sfn_client = MagicMock() mock_sfn_client.start_execution.side_effect = Exception('Mocked boto3 exception') mocker.patch('src.logic.step_function.get_sfn_client', return_value=mock_sfn_client) # After line 60 in app.py, add_inputs are extended to other_indexing_inputs # So order is: deindex items first, then add_to_index items expected_inputs = [ i for i in mock_indexing_data_inputs if i['operation_type'] != ADD_INDEX_OP ] + [ i for i in mock_indexing_data_inputs if i['operation_type'] == ADD_INDEX_OP ] with pytest.raises(Exception) as er: app.handler(mock_cdc_event, None) assert str(er.value) == ( 'Failed to start step function: Mocked boto3 exception, ' f"input: {{'data': {{'indexing_inputs': {expected_inputs}}}}}" ) def test_handler_with_store_product_enabled( mock_cdc_event, mock_indexing_data_inputs, mock_sfn_execution_arn, mock_sqs_add_result, mocker ): """Test handler function when store-product feature flag is enabled.""" from src import app from src.logic import feature mocker.patch.object( app, 'content_lambda_logger', new=app_content_lambda_logger, ) mocker.patch.object( feature, 'is_store_product_enabled', return_value=True, ) mocker.patch.object( app, 'process_event', side_effect=[None] + mock_indexing_data_inputs, ) mocker.patch.object( app.sqs, 'add_to_store_product_queue', return_value=mock_sqs_add_result, ) mocker.patch.object( app.step_function, 'trigger_sfn', return_value=mock_sfn_execution_arn, ) mock_app_logger = mocker.patch.object(app, 'app_logger') result = app.handler(mock_cdc_event, None) feature.is_store_product_enabled.assert_called_once() app.sqs.add_to_store_product_queue.assert_called_once_with({ 'product_id': 3, 'review_queue_id': 3, 'payload': { 'id': 3, 'product_id': 3, 'status': 'new', 'created_datetime': '2021-08-03T01:13:14Z' }, 'operation_type': 'add_to_index' }) step_function.trigger_sfn.assert_called_with({ 'data': { 'indexing_inputs': [ { 'product_id': 2, 'review_queue_id': 2, 'payload': None, 'operation_type': 'deindex' }, { 'product_id': 4, 'review_queue_id': 4, 'payload': None, 'operation_type': 'deindex' } ] } }) # Verify app_logger.info was called correctly execution_arn = mock_sfn_execution_arn['executionArn'] assert mock_app_logger.info.call_count == 3 mock_app_logger.info.assert_any_call( 'published to sqs: product_id=3 review_queue_id=3 ' 'sqs=content-review-store-product-queue MessageId=Random-mock-id-123' ) mock_app_logger.info.assert_any_call( f'sfn execution: product_id=2 review_queue_id=2 execution_arn={execution_arn}' ) mock_app_logger.info.assert_any_call( f'sfn execution: product_id=4 review_queue_id=4 execution_arn={execution_arn}' ) assert result == {'status': 'OK'} def test_handler_with_store_product_enabled_sqs_error( mock_cdc_event, mock_indexing_data_inputs, mock_sfn_execution_arn, mocker ): """Test handler when store-product is enabled but SQS publish fails.""" from src import app from src.logic import feature mocker.patch.object( app, 'content_lambda_logger', new=app_content_lambda_logger, ) mocker.patch.object( feature, 'is_store_product_enabled', return_value=True, ) mocker.patch.object( app, 'process_event', side_effect=[None] + mock_indexing_data_inputs, ) mocker.patch.object( app.sqs, 'add_to_store_product_queue', side_effect=Exception('SQS publish failed'), ) mocker.patch.object( app.step_function, 'trigger_sfn', return_value=mock_sfn_execution_arn, ) mock_app_logger = mocker.patch.object(app, 'app_logger') result = app.handler(mock_cdc_event, None) # Verify SQS was attempted feature.is_store_product_enabled.assert_called_once() app.sqs.add_to_store_product_queue.assert_called_once_with({ 'product_id': 3, 'review_queue_id': 3, 'payload': { 'id': 3, 'product_id': 3, 'status': 'new', 'created_datetime': '2021-08-03T01:13:14Z' }, 'operation_type': 'add_to_index' }) # Verify error was logged (handler catches and logs the exception) mock_app_logger.error.assert_called_once_with( 'error: failed to publish to sqs product_id=3 review_queue_id=3 ' 'sqs=content-review-store-product-queue SQS publish failed' ) # Verify step function called with all original items expected_inputs = [ i for i in mock_indexing_data_inputs if i['operation_type'] != ADD_INDEX_OP ] failed_inputs = [ i for i in mock_indexing_data_inputs if i['operation_type'] == ADD_INDEX_OP ] expected_inputs.extend(failed_inputs) step_function.trigger_sfn.assert_called_with({ 'data': { 'indexing_inputs': expected_inputs } }) # Verify status is 'error' due to SQS failure assert result == {'status': 'error'} # Verify app_logger.info was called for all step function executions execution_arn = mock_sfn_execution_arn['executionArn'] assert mock_app_logger.info.call_count == 3 mock_app_logger.info.assert_any_call( f'sfn execution: product_id=2 review_queue_id=2 execution_arn={execution_arn}' ) mock_app_logger.info.assert_any_call( f'sfn execution: product_id=3 review_queue_id=3 execution_arn={execution_arn}' ) mock_app_logger.info.assert_any_call( f'sfn execution: product_id=4 review_queue_id=4 execution_arn={execution_arn}' ) def test_start_store_product_flow(mocker): """Test start_store_product_flow function.""" from src import app from src.logic import sqs mock_inputs = [ {'product_id': 1, 'review_queue_id': 1}, {'product_id': 2, 'review_queue_id': 2} ] # Setup mocks mock_sqs_add = mocker.patch.object( sqs, 'add_to_store_product_queue', return_value={'MessageId': '123'} ) mock_logger = mocker.patch.object(app, 'app_logger') # 1. Test Success status, failures = app.start_store_product_flow(mock_inputs) assert status == 'OK' assert failures == [] assert mock_sqs_add.call_count == 2 assert mock_logger.info.call_count == 2 # 2. Test Failure (side_effect) mock_sqs_add.side_effect = Exception('SQS Error') mock_sqs_add.reset_mock() mock_logger.reset_mock() status, failures = app.start_store_product_flow(mock_inputs) assert status == 'error' assert failures == mock_inputs assert mock_sqs_add.call_count == 2 assert mock_logger.error.call_count == 2