"""Unit tests for Lambda handler.""" from unittest.mock import MagicMock, Mock, patch import pytest from src import app from src.errors import PermanentError, TransientError def _make_event(sppe_id: int = 1) -> dict: """Create an EventBridge event for testing.""" return { 'detail-type': 'close_balance.completed', 'detail': { 'metadata': { 'target_id': sppe_id, 'target_type': 'statement_period_payment_entity', }, }, } class TestHandler: """Tests for Lambda handler function.""" @patch('src.app.PaymentAllocationProcessor') @patch('src.app.Repository') @patch('src.app.MySQLConnectionFactory') def test_handler_success( self, mock_factory_class, mock_repository_class, mock_processor_class ): """Test successful lambda handler execution.""" mock_conn = MagicMock() mock_factory_class.return_value.connection.return_value.__enter__.return_value = mock_conn mock_repository = Mock() mock_repository_class.return_value = mock_repository mock_processor = Mock() mock_result = Mock() mock_result.model_dump.return_value = { 'statement_period_id': 456, 'statement_period_payment_entity_id': 1, 'allocations_created': 5, 'ledger_adjustments_linked': 15, } mock_processor.process.return_value = mock_result mock_processor_class.return_value = mock_processor result = app.handler(_make_event(), Mock()) assert result['statement_period_id'] == 456 assert result['statement_period_payment_entity_id'] == 1 assert result['allocations_created'] == 5 assert result['ledger_adjustments_linked'] == 15 mock_repository_class.assert_called_once_with(mock_conn) mock_processor_class.assert_called_once_with(mock_repository) @patch('src.app.PaymentAllocationProcessor') @patch('src.app.Repository') @patch('src.app.MySQLConnectionFactory') def test_handler_success_simple_event( self, mock_factory_class, mock_repository_class, mock_processor_class ): """Test successful handler execution with simple event format.""" mock_conn = MagicMock() mock_factory_class.return_value.connection.return_value.__enter__.return_value = mock_conn mock_repository = Mock() mock_repository_class.return_value = mock_repository mock_processor = Mock() mock_result = Mock() mock_result.model_dump.return_value = { 'statement_period_id': 456, 'statement_period_payment_entity_id': 99, 'allocations_created': 3, 'ledger_adjustments_linked': 10, } mock_processor.process.return_value = mock_result mock_processor_class.return_value = mock_processor result = app.handler({'statement_period_payment_entity_id': 99}, Mock()) assert result['statement_period_payment_entity_id'] == 99 assert result['allocations_created'] == 3 mock_processor.process.assert_called_once_with(99) @patch('src.app.MySQLConnectionFactory') def test_handler_validation_error(self, mock_factory_class): """Test handler with invalid event raises PermanentError.""" with pytest.raises(PermanentError): app.handler({'invalid': 'event'}, Mock()) @patch('src.app.MySQLConnectionFactory') def test_handler_missing_fields(self, mock_factory_class): """Test handler with missing required fields raises PermanentError.""" with pytest.raises(PermanentError): app.handler({}, Mock()) @patch('src.app.MySQLConnectionFactory') def test_handler_null_event(self, mock_factory_class): """Test handler with None event raises PermanentError.""" with pytest.raises(PermanentError): app.handler(None, Mock()) @patch('src.app.PaymentAllocationProcessor') @patch('src.app.Repository') @patch('src.app.MySQLConnectionFactory') def test_handler_processor_error_rolls_back( self, mock_factory_class, mock_repository_class, mock_processor_class ): """Test handler rolls back on processor error.""" mock_conn = MagicMock() mock_factory_class.return_value.connection.return_value.__enter__.return_value = mock_conn mock_repository = Mock() mock_repository_class.return_value = mock_repository mock_processor = Mock() mock_processor.process.side_effect = PermanentError('test error') mock_processor_class.return_value = mock_processor with pytest.raises(PermanentError): app.handler(_make_event(), Mock()) mock_conn.rollback.assert_called_once() mock_conn.commit.assert_not_called() @patch('src.app.PaymentAllocationProcessor') @patch('src.app.Repository') @patch('src.app.MySQLConnectionFactory') def test_handler_transient_error_propagates( self, mock_factory_class, mock_repository_class, mock_processor_class ): """Test handler propagates TransientError for retry.""" mock_conn = MagicMock() mock_factory_class.return_value.connection.return_value.__enter__.return_value = mock_conn mock_repository = Mock() mock_repository_class.return_value = mock_repository mock_processor = Mock() mock_processor.process.side_effect = TransientError('DB connection lost') mock_processor_class.return_value = mock_processor with pytest.raises(TransientError): app.handler(_make_event(), Mock()) mock_conn.rollback.assert_called_once() mock_conn.commit.assert_not_called() @patch('src.app.PaymentAllocationProcessor') @patch('src.app.Repository') @patch('src.app.MySQLConnectionFactory') def test_handler_logs_processing_time( self, mock_factory_class, mock_repository_class, mock_processor_class ): """Test handler logs processing time.""" mock_conn = MagicMock() mock_factory_class.return_value.connection.return_value.__enter__.return_value = mock_conn mock_repository = Mock() mock_repository_class.return_value = mock_repository mock_processor = Mock() mock_result = Mock() mock_result.model_dump.return_value = { 'statement_period_id': 456, 'statement_period_payment_entity_id': 1, 'allocations_created': 0, 'ledger_adjustments_linked': 0, } mock_processor.process.return_value = mock_result mock_processor_class.return_value = mock_processor with patch('src.app.logger') as mock_logger: app.handler(_make_event(), Mock()) log_calls = [call.args[0] for call in mock_logger.info.call_args_list] assert any('Finished processing in' in call for call in log_calls)