"""Lambda test module.""" from datetime import datetime, timedelta from unittest.mock import patch import pytest from freezegun import freeze_time from src import app from src.queries.sql_queries import CHECK_HFA_PROCESS_FOR_DATE, INSERT_HFA_ORCHARD_TRACK_LICENSES from src.utils import constants @patch('src.logic.hfa.get_pending_hfa_request') @patch('src.app.insert_pending_hfa_request_to_publishing') @patch('src.app.is_datetime_valid') @patch('src.app.has_already_processed') def test_handler_api( mock_has_already_processed, mock_is_datetime_valid, mock_insert_pending_hfa_request_to_publishing, mock_get_pending_hfa_request, ): """Test handler execution.""" mock_is_datetime_valid.return_value = True mock_has_already_processed.return_value = False mock_get_pending_hfa_request.return_value = [ {constants.FIELD_TRACK_ID: 456, constants.FIELD_HFA_CONF_CODE: 'DL'} ] test_event = {'datetime': '2025-05-03 15:00:00'} result = app.handler(test_event, {}) mock_has_already_processed.assert_called_once() mock_get_pending_hfa_request.assert_called_once() mock_insert_pending_hfa_request_to_publishing.assert_called_once() assert result['status'] == 'success' assert result['records_processed'] == 1 @freeze_time('2025-05-02 12:00:00') @patch('src.app.hfa.get_pending_hfa_request') @patch('src.app.insert_pending_hfa_request_to_publishing') @patch('src.app.has_already_processed') @patch('src.app.is_datetime_valid') def test_handler_empty_event( mock_is_datetime_valid, mock_has_already_processed, mock_insert_pending_hfa_request_to_publishing, mock_get_pending_hfa_request, ): """Test handler uses the default current time when event is empty.""" mock_has_already_processed.return_value = False mock_get_pending_hfa_request.return_value = [ {constants.FIELD_TRACK_ID: 123, constants.FIELD_HFA_CONF_CODE: 'SP'} ] test_event = {} result = app.handler(test_event, {}) assert not mock_is_datetime_valid.called mock_has_already_processed.assert_called_once_with('2025-05-02 12:00:00') mock_get_pending_hfa_request.assert_called_once() mock_insert_pending_hfa_request_to_publishing.assert_called_once() assert result['status'] == 'success' assert result['records_processed'] == 1 @patch('src.app.has_already_processed') @patch('src.app.logger.info') @patch('src.app.is_datetime_valid') def test_handler_already_processed(mock_is_datetime_valid, mock_logger_info, mock_has_already_processed): """Test handler skips processing when already processed for the datetime.""" mock_is_datetime_valid.return_value = True mock_has_already_processed.return_value = True current_datetime = '2025-05-02 12:00:00' test_event = {'datetime': current_datetime} result = app.handler(test_event, {}) assert result['status'] == 'skipped' assert result['message'] == f'Already processed for {current_datetime}.' mock_logger_info.assert_any_call('Already processed for this datetime. Exiting.') mock_has_already_processed.assert_called_once_with(current_datetime) @patch('src.app.ses.send_email') @patch('src.app.logger.exception') @patch('src.app.capture_exception') @patch('src.app.hfa.get_pending_hfa_request') @patch('src.app.has_already_processed') @patch('src.app.is_datetime_valid') def test_handler_exception_block( mock_is_datetime_valid, mock_has_already_processed, mock_get_pending_hfa_request, mock_capture_exception, mock_logger_exception, mock_send_email ): """Test handler exception block is triggered and handled correctly.""" mock_is_datetime_valid.return_value = True mock_has_already_processed.return_value = False mock_get_pending_hfa_request.side_effect = Exception('Something went wrong') test_event = {'datetime': '2025-05-02 10:24:42'} with pytest.raises(Exception, match='Something went wrong'): app.handler(test_event, {}) mock_capture_exception.assert_called_once() mock_logger_exception.assert_called_once() mock_send_email.assert_called_once_with( recipients=app.config.EMAIL_RECIPIENTS, sender=app.config.EMAIL_SENDER, subject=( app.constants.MAIL_SUBJECT if app.config.ENVIRONMENT == app.config.PROD_ENVIRONMENT else f'{app.constants.MAIL_SUBJECT} ({app.config.ENVIRONMENT})' ), message='Something went wrong' ) @patch('src.app.ses.send_email') @patch('src.app.logger.exception') @patch('src.app.capture_exception') @patch('src.app.is_datetime_valid') def test_handler_invalid_datetime( mock_is_datetime_valid, mock_capture_exception, mock_logger_exception, mock_send_email, ): """Test handler raises ValueError if datetime is invalid.""" mock_is_datetime_valid.return_value = False test_event = {'datetime': '2025-05-02 15:00:00'} with pytest.raises(ValueError, match='Provided datetime must be within the last'): app.handler(test_event, {}) mock_is_datetime_valid.assert_called_once_with('2025-05-02 15:00:00') @patch('src.app.util.mysql_connection') def test_insert_pending_hfa_request_to_publishing(mysql_connection): """Test app.insert_pending_hfa_request_to_publishing function.""" result = [ {constants.FIELD_TRACK_ID: 1, constants.FIELD_HFA_CONF_CODE: 'CD'}, {constants.FIELD_TRACK_ID: 2, constants.FIELD_HFA_CONF_CODE: 'SP'}, {constants.FIELD_TRACK_ID: 3, constants.FIELD_HFA_CONF_CODE: 'CD'}, ] app.insert_pending_hfa_request_to_publishing(result) conn = mysql_connection.return_value.__enter__.return_value cursor = conn.cursor.return_value.__enter__.return_value expected_values = [ (1, constants.STATE, 'CD'), (2, constants.STATE, 'SP'), (3, constants.STATE, 'CD'), ] assert conn.commit.call_count == 1 cursor.executemany.assert_called_once_with( INSERT_HFA_ORCHARD_TRACK_LICENSES, expected_values ) @patch('src.app.util.mysql_connection') def test_insert_pending_hfa_request_to_publishing_exception(mysql_connection): """Test exception handling in insert_pending_hfa_request_to_publishing.""" result = [{constants.FIELD_TRACK_ID: 1, constants.FIELD_HFA_CONF_CODE: 'CD'}] conn = mysql_connection.return_value.__enter__.return_value cursor = conn.cursor.return_value.__enter__.return_value cursor.executemany.side_effect = Exception('DB Insert Failed') with pytest.raises(Exception) as exc_info: app.insert_pending_hfa_request_to_publishing(result) assert 'DB Insert Failed' in str(exc_info.value) cursor.executemany.assert_called_once() @freeze_time('2025-05-02 12:00:00') @pytest.mark.parametrize( 'input_offset_days,expected', [ pytest.param(-1, True, id='valid_within_lookback'), pytest.param(-8, False, id='too_old_beyond_lookback'), pytest.param(1, False, id='future_date_invalid'), ] ) def test_is_datetime_valid_with_offsets(input_offset_days, expected): """Test is_datetime_valid with dates relative to a frozen datetime.""" app.config.DAYS_LOOKBACK = 7 test_datetime = (datetime.now() + timedelta(days=input_offset_days)).strftime('%Y-%m-%d %H:%M:%S') assert app.is_datetime_valid(test_datetime) is expected @pytest.mark.parametrize( 'invalid_str', [ pytest.param('2025/05/02 12:00:00', id='wrong_format_slashes'), pytest.param('not-a-date', id='nonsense_string'), pytest.param('', id='empty_string'), pytest.param('2025-13-01 00:00:00', id='invalid_month'), pytest.param('2025-01-32 00:00:00', id='invalid_day'), ] ) def test_is_datetime_valid_invalid_format(invalid_str): """Test is_datetime_valid with invalid formats.""" assert app.is_datetime_valid(invalid_str) is False @patch('src.app.util.mysql_connection') def test_has_already_processed_true(mock_mysql_connection): """Test has_already_processed returns True when count > 0.""" mock_conn = mock_mysql_connection.return_value.__enter__.return_value mock_cursor = mock_conn.cursor.return_value.__enter__.return_value mock_cursor.fetchone.return_value = {'cnt': 1} dt = datetime(2025, 5, 2, 12, 0, 0) result = app.has_already_processed(dt) assert result is True mock_cursor.execute.assert_called_once_with(CHECK_HFA_PROCESS_FOR_DATE, (dt,)) @patch('src.app.util.mysql_connection') def test_has_already_processed_false(mock_mysql_connection): """Test has_already_processed returns False when count == 0.""" mock_conn = mock_mysql_connection.return_value.__enter__.return_value mock_cursor = mock_conn.cursor.return_value.__enter__.return_value mock_cursor.fetchone.return_value = {'cnt': 0} dt = datetime(2025, 5, 2, 12, 0, 0) result = app.has_already_processed(dt) assert result is False @patch('src.app.util.mysql_connection') def test_has_already_processed_raises_exception(mock_mysql_connection): """Test has_already_processed logs and raises on DB error.""" mock_conn = mock_mysql_connection.return_value.__enter__.return_value mock_cursor = mock_conn.cursor.return_value.__enter__.return_value mock_cursor.execute.side_effect = Exception('DB error') with pytest.raises(Exception, match='DB error'): app.has_already_processed(datetime(2025, 5, 2, 12, 0, 0))