"""Lambda test module.""" from typing import Any from unittest.mock import MagicMock, patch import pytest from pydantic import ValidationError from app import handler from config import config from sync_contract_sap.schemas import BatchSyncResponse @patch('app.SyncContractSAPProcessor') def test_handler(mock_processor: Any, mock_event: dict[str, Any]) -> None: """Test handler function.""" result = handler(mock_event, None) assert result == {'status': 'OK'} mock_processor.assert_called_once_with() mock_processor.return_value.process.assert_called_once_with( int(mock_event['target_id']) ) @patch('app.SyncContractSAPProcessor.process') def test_handler_exception( mock_process: Any, mock_event: dict[str, Any], ) -> None: """Test handler re-raises processor exceptions.""" mock_process.side_effect = Exception('key error') with pytest.raises(Exception, match='key error'): handler(mock_event, None) @patch('app.time.monotonic') def test_batch_deadline_computed_from_context(mock_monotonic: Any) -> None: """Test the deadline is remaining time minus the floor, from now.""" from app import _batch_deadline from sync_contract_sap import constants mock_monotonic.return_value = 1000.0 context = MagicMock() context.get_remaining_time_in_millis.return_value = 300_000 deadline = _batch_deadline(context) expected_budget = (300_000 - constants.BATCH_REMAINING_TIME_FLOOR_MS) / 1000 assert deadline == 1000.0 + expected_budget context.get_remaining_time_in_millis.assert_called_once() def test_batch_deadline_none_without_context() -> None: """Test no deadline is computed when there is no Lambda context.""" from app import _batch_deadline assert _batch_deadline(None) is None @patch('app.mysql_connection') def test_handler_rejects_non_numeric_target_id(mock_mysql_ctx: Any) -> None: """Test a non-numeric target_id raises and does not fall through to batch.""" with pytest.raises(ValidationError): handler({'target_id': 'abc'}, None) mock_mysql_ctx.assert_not_called() @patch('app.mysql_connection') def test_handler_rejects_zero_target_id(mock_mysql_ctx: Any) -> None: """Test target_id=0 raises a validation error instead of running a batch.""" with pytest.raises(ValidationError): handler({'target_id': 0}, None) mock_mysql_ctx.assert_not_called() @patch('app.mysql_connection') def test_handler_rejects_negative_target_id(mock_mysql_ctx: Any) -> None: """Test a negative target_id raises and does not fall through to batch.""" with pytest.raises(ValidationError): handler({'target_id': -1}, None) mock_mysql_ctx.assert_not_called() @patch('app.Repository') @patch('app.mysql_connection') @patch('app.SyncContractSAPProcessor') def test_handler_batch_mode( mock_processor_cls: Any, mock_mysql_ctx: Any, mock_repo_cls: Any ) -> None: """Test handler routes to batch mode when no target_id in event.""" mock_rows = [{'contract_id': 1, 'abacus_state_id': 10}] mock_conn = MagicMock() mock_mysql_ctx.return_value.__enter__ = MagicMock(return_value=mock_conn) mock_mysql_ctx.return_value.__exit__ = MagicMock(return_value=False) mock_repo = MagicMock() mock_repo.get_contracts_pending_sap_sync.return_value = mock_rows mock_repo_cls.return_value = mock_repo mock_processor = MagicMock() response = BatchSyncResponse(total=1, success=1, skipped=0, errors=[]) mock_processor.process_batch.return_value = response mock_processor_cls.return_value = mock_processor result = handler({}, None) mock_repo.get_contracts_pending_sap_sync.assert_called_once_with( config.batch.MAX_BATCH_SIZE, config.batch.STALE_SYNC_MINUTES ) mock_processor.process_batch.assert_called_once_with(mock_rows, None) assert result == {'total': 1, 'success': 1, 'skipped': 0, 'errors': []} @patch('app.Repository') @patch('app.mysql_connection') @patch('app.SyncContractSAPProcessor') def test_handler_batch_mode_raises_on_db_error( mock_processor_cls: Any, mock_mysql_ctx: Any, mock_repo_cls: Any ) -> None: """Test handler re-raises when the DB connection fails.""" mock_mysql_ctx.return_value.__enter__ = MagicMock( side_effect=Exception('DB connection failed') ) mock_mysql_ctx.return_value.__exit__ = MagicMock(return_value=False) with pytest.raises(Exception, match='DB connection failed'): handler({}, None)