"""Test event_source_mapping.""" import logging from typing import Any from unittest.mock import call from unittest.mock import patch from unittest.mock import MagicMock import pytest from src.event_source_mapping import _create_event_source_mapping from src.event_source_mapping import _delete_event_source_mappings from src.event_source_mapping import _get_dynamodb_stream_arn from src.event_source_mapping import _get_relevant_event_source_mapping_uuids from src.event_source_mapping import set_lambda_event_source_mapping from src.event_source_mapping import NewEventSourceMappingFailure from src.event_source_mapping import NoDynamodbStreamFailure @pytest.mark.parametrize( 'describe_table_response,' 'expect_exception,' 'expected_value,' 'expected_caplog_messages', [ pytest.param( {}, True, None, [ 'payload missing LatestStreamArn for my_pp_table', 'missing stream arn for my_pp_table', ], id='empty response means no stream arn' ), pytest.param( {'Table': {'LatestStreamArn': '', 'TableStatus': 'UPDATING'}}, True, None, [ 'payload missing LatestStreamArn for my_pp_table', 'missing stream arn for my_pp_table', ], id='empty string means no stream arn' ), pytest.param( {'Table': {'LatestStreamArn': 'hello-pp-team'}}, False, 'hello-pp-team', [ 'Found latest dynamodb stream arn: hello-pp-team', ], id='happy path' ), pytest.param( Exception('some dynamo exception'), True, None, [ 'missing stream arn for my_pp_table', ], id='an exception is raised and re-raised during execution' ) ] ) def test__get_dynamodb_stream_arn( describe_table_response: Any, expect_exception: bool, expected_value: str, expected_caplog_messages: list[str], caplog: pytest.LogCaptureFixture, ) -> None: """Test _get_dynamodb_stream_arn.""" mock_client = MagicMock() mock_client.describe_table.side_effect = [describe_table_response] table_name = 'my_pp_table' with caplog.at_level(logging.INFO): if expect_exception: with pytest.raises(NoDynamodbStreamFailure): _get_dynamodb_stream_arn(mock_client, table_name) else: actual = _get_dynamodb_stream_arn(mock_client, table_name) assert actual == expected_value mock_client.describe_table.assert_called_once_with(TableName=table_name) assert 'Fetching latest dynamodb stream arn' in caplog.messages for expected_message in expected_caplog_messages: assert expected_message in caplog.messages @pytest.mark.parametrize( 'list_event_source_mapping_response, expected_uuids', [ pytest.param({}, [], id='empty response returns empty list'), pytest.param(Exception('aws problems'), [], id='exception returns empty list'), pytest.param({ 'EventSourceMappings': [ { 'UUID': 'uuid1', 'EventSourceArn': 'non-matching', }, { 'UUID': 'uuid2', 'EventSourceArn': 'arn:aws:dynamodb:non-matching', }, { 'UUID': 'uuid3', 'EventSourceArn': 'arn:aws:dynamodb:stuff:this_my_table/stream/1234', } ] }, ['uuid3'], id='only one of 3 event source mappings matched'), ] ) def test__get_relevant_event_source_mapping_uuids( list_event_source_mapping_response: Any, expected_uuids: list[str], ) -> None: """Test _get_relevant_event_source_mapping_uuids.""" mock_client = MagicMock() mock_client.list_event_source_mappings.side_effect = [ list_event_source_mapping_response] lambda_arn = 'some_fn_arn' table_name = 'this_my_table' actual = _get_relevant_event_source_mapping_uuids( mock_client, lambda_arn, table_name, ) mock_client.list_event_source_mappings.assert_called_once_with( FunctionName=lambda_arn ) assert actual == expected_uuids def test__delete_event_source_mappings( caplog: pytest.LogCaptureFixture ) -> None: """Test _delete_event_source_mappings.""" mock_client = MagicMock() mock_client.delete_event_source_mapping.side_effect = [ None, Exception('could not find it to delete it'), None, ] with caplog.at_level(logging.INFO): _delete_event_source_mappings(mock_client, [ 'uuid1', 'uuid2', 'uuid3', ]) assert "Deleting old event source mappings: ['uuid1', 'uuid2', 'uuid3']"\ in caplog.messages assert 'Failed to delete an event source mapping: uuid2' in caplog.messages mock_client.delete_event_source_mapping.assert_has_calls([ call(UUID='uuid1'), call(UUID='uuid2'), call(UUID='uuid3'), ]) @pytest.mark.parametrize( 'create_event_source_response, expect_exception', [ pytest.param( Exception('something'), True, id='an exception raises a named exception', ), pytest.param( {'does': 'not look at response body'}, False, id='happy path', ), ], ) def test__create_event_source_mapping( create_event_source_response: Any, expect_exception: bool, ) -> None: """Test _create_event_source_mapping.""" mock_client = MagicMock() mock_client.create_event_source_mapping.side_effect = [ create_event_source_response ] dynamodb_stream_arn = 'pp_stream_arn' target_lambda_arn = 'some_lambda_fn_arn' additional_configs = { 'IfThisWereReal': 'the client would complain about the value', 'ButItsAMock': 'A real mock', } if expect_exception: with pytest.raises(NewEventSourceMappingFailure): _create_event_source_mapping( mock_client, target_lambda_arn, dynamodb_stream_arn, **additional_configs, ) else: _create_event_source_mapping( mock_client, target_lambda_arn, dynamodb_stream_arn, **additional_configs, ) mock_client.create_event_source_mapping.assert_called_once_with( EventSourceArn=dynamodb_stream_arn, FunctionName=target_lambda_arn, Enabled=True, **additional_configs, ) @patch('src.event_source_mapping._delete_event_source_mappings') @patch('src.event_source_mapping._create_event_source_mapping') @patch('src.event_source_mapping._get_relevant_event_source_mapping_uuids') @patch('src.event_source_mapping._get_dynamodb_stream_arn') def test_set_lambda_event_source_mapping_no_stream( mock_get_dynamodb_stream_arn: MagicMock, mock_get_relevant_event_source_mapping_uuids: MagicMock, mock_create_event_source_mapping: MagicMock, mock_delete_event_source_mappings: MagicMock, ) -> None: """Test set_lambda_event_source_mapping when there is no stream.""" mock_get_dynamodb_stream_arn.side_effect = [ NoDynamodbStreamFailure('stop early'), ] mock_dynamodb_client = MagicMock() set_lambda_event_source_mapping( mock_dynamodb_client, 'dynamodb table', MagicMock(), 'target_lambda_arn', ) mock_get_dynamodb_stream_arn.assert_called_once_with( mock_dynamodb_client, 'dynamodb table') mock_get_relevant_event_source_mapping_uuids.assert_not_called() mock_create_event_source_mapping.assert_not_called() mock_delete_event_source_mappings.assert_not_called() @pytest.mark.parametrize('relevant_uuids', [ pytest.param([], id='no existing mappings to delete'), pytest.param(['uuid1'], id='non-empty list of mappings to delete') ]) @patch('src.event_source_mapping._delete_event_source_mappings') @patch('src.event_source_mapping._create_event_source_mapping') @patch('src.event_source_mapping._get_relevant_event_source_mapping_uuids') @patch('src.event_source_mapping._get_dynamodb_stream_arn') def test_set_lambda_event_source_mapping( mock_get_dynamodb_stream_arn: MagicMock, mock_get_relevant_event_source_mapping_uuids: MagicMock, mock_create_event_source_mapping: MagicMock, mock_delete_event_source_mappings: MagicMock, relevant_uuids: list[str], ) -> None: """Test set_lambda_event_source_mapping.""" table_name = 'the source' table_stream_arn = 'the source arn' lambda_arn = 'the lambda fn' extra = {'this': 'that'} mock_get_dynamodb_stream_arn.side_effect = [ table_stream_arn ] mock_get_relevant_event_source_mapping_uuids.return_value = relevant_uuids mock_dynamodb_client = MagicMock() mock_lambda_client = MagicMock() set_lambda_event_source_mapping( mock_dynamodb_client, table_name, mock_lambda_client, lambda_arn, **extra, ) mock_get_dynamodb_stream_arn.assert_called_once_with( mock_dynamodb_client, table_name) mock_get_relevant_event_source_mapping_uuids.assert_called_once_with( mock_lambda_client, lambda_arn, table_name, ) mock_create_event_source_mapping.assert_called_once_with( mock_lambda_client, lambda_arn, table_stream_arn, **extra, ) mock_delete_event_source_mappings.assert_called_once_with( mock_lambda_client, relevant_uuids, )