"""Unit tests for Sentry/Raven based on_exception call handler.""" from unittest.mock import call, MagicMock from unittest.mock import patch import freezegun import pytest from feed_ingestion.util import sentry_util def test_ttl_dict(): """Test TTLDict functionality.""" with freezegun.freeze_time('2025-10-11 12:00:00'): ttl_dict = sentry_util.TTLDict(ttl_seconds=60) ttl_dict['key1'] = 'value1' assert ttl_dict['key1'] == 'value1' assert len(ttl_dict._store) == 1 # Check that the key is still there after a short time with freezegun.freeze_time('2025-10-11 12:00:01'): assert ttl_dict['key1'] == 'value1' assert len(ttl_dict._store) == 1 # Check that the key is still there close to TTL with freezegun.freeze_time('2025-10-11 12:00:59'): assert ttl_dict['key1'] == 'value1' assert len(ttl_dict._store) == 1 # After TTL expires, the key should be removed with freezegun.freeze_time('2025-10-11 13:01:01'): assert 'key1' not in ttl_dict assert len(ttl_dict._store) == 0, 'item should be deleted after TTL' def test_ttl_dict_cleanup(): with freezegun.freeze_time('2025-10-11 12:00:00'): ttl_dict = sentry_util.TTLDict(ttl_seconds=60) assert len(ttl_dict._store) == 0 ttl_dict['key1'] = 'value1' assert len(ttl_dict._store) == 1 with freezegun.freeze_time('2025-10-11 12:01:01'): assert len(ttl_dict._store) == 1, 'item is still here although expired' ttl_dict.cleanup() assert len(ttl_dict._store) == 0, 'item should be cleaned up' @pytest.mark.parametrize( 'exception_type, message, expected', [ pytest.param( 'ProgrammingError', 'has locked table', True, id='ProgrammingError - should send as warning'), pytest.param( 'some.module.ProgrammingError', 'has locked table', True, id='some.module.ProgrammingError - should send as warning'), pytest.param( 'FileExistsError', 'Some actionable exception', False, id='FileExistsError - should send normaly'), ] ) def test_is_send_as_warning(exception_type, message, expected): """Test _is_send_as_warning function.""" assert sentry_util._is_send_as_warning( exception_type, message) == expected @pytest.mark.parametrize( 'exception_type, message, expected', [ pytest.param( 'UnknownResourceFault', 'An error occurred (UnknownResourceFault) when calling the ' 'RespondActivityTaskFailed operation: Unknown execution: ' 'WorkflowExecution=[workflowId=apple_music_feed_ingestion', True, id='UnknownResourceFault - should not send'), pytest.param( 'botocore.errorfactory.UnknownResourceFault', 'An error occurred (UnknownResourceFault) when calling the ' 'RespondActivityTaskFailed operation: Unknown execution: ' 'WorkflowExecution=[workflowId=apple_music_feed_ingestion', True, id='botocore.errorfactory.UnknownResourceFault - should not send'), pytest.param( 'ProgrammingError', 'has locked table', False, id='ProgrammingError - should send'), ] ) def test_is_not_to_send(exception_type, message, expected): """Test _is_send_as_warning function.""" result = sentry_util._is_not_to_send(exception_type, message) assert result == expected @patch('feed_ingestion.util.sentry_util.sentry_sdk') def test_send_error_or_warning(sentry_sdk_mock, monkeypatch): """Test send_error_or_warning function.""" # test send as warning monkeypatch.setenv('SENTRY_DSN', 'https://sentry.io') exception = Exception('The activity failures has exceeded its retry limit') sentry_util.send_error_or_warning(exception) assert sentry_sdk_mock.capture_exception.call_args_list == [ call(exception, level='error') ] @pytest.mark.parametrize( 'kwargs, expected_kwargs', [ pytest.param( {'level': 'warning'}, {'level': 'warning'}, id='force send as warning'), pytest.param( {'level': 'error'}, {'level': 'error'}, id='not force warning == error'), pytest.param( {}, {'level': 'error'}, id='default to error'), ] ) @patch('feed_ingestion.util.sentry_util.sentry_sdk') def test_send_message( sentry_sdk_mock, kwargs, expected_kwargs, monkeypatch): """Test send_error_or_warning function.""" # test send as warning monkeypatch.setenv('SENTRY_DSN', 'https://sentry.io') message = 'something wrong' sentry_util.send_message(message, **kwargs) assert sentry_sdk_mock.capture_message.call_args_list == [ call(message, **expected_kwargs) ] @pytest.mark.parametrize( 'event, event_passed, expected_level', [ pytest.param( { 'exception': { 'values': [ { 'type': 'Exception', 'value': 'The activity failures ' 'has exceeded its retry limit' }] }, 'level': 'error', }, False, None, id='exception - filtered'), pytest.param( { 'exception': { 'values': [ { 'type': 'ValueError', 'value': 'Some other exception' }] }, 'level': 'error', }, True, 'error', id='exception - not filtered'), pytest.param( { 'exception': { 'values': [ { 'type': 'Exception', 'value': 'The activity failures ' 'has exceeded its retry limit' }, { 'type': 'ValueError', 'value': 'Chained exception' }] }, 'level': 'error', }, True, 'error', id='chained exception - should take the last one - NOT filtered'), pytest.param( { 'exception': { 'values': [{ 'type': 'SSHException', 'value': 'Error reading SSH protocol banner' }] }, 'level': 'error', }, True, 'warning', id='exception - reduced to warning'), pytest.param( { 'message': 'something went wrong', 'level': 'error', }, True, 'error', id='message error - pass'), pytest.param( { 'message': 'warning message', 'level': 'warning', }, True, 'warning', id='message warning - pass'), pytest.param( { 'logentry': { 'formatted': 'An error occurred while processing the request' }, 'level': 'error', }, True, 'error', id='log entry - pass'), ] ) def test_before_send(event, event_passed, expected_level): """Test before_send function.""" result = sentry_util.before_send(event, MagicMock()) if event_passed: expected_result = event.copy() if expected_level: expected_result['level'] = expected_level else: expected_result = None assert result == expected_result @pytest.mark.parametrize( 'event', [ pytest.param( { 'exception': { 'values': [{ 'type': 'ValueError', 'value': 'Some other exception' }] }, 'level': 'error', }, id='exception'), pytest.param( { 'message': 'something went wrong', 'level': 'error', }, id='message error'), pytest.param( { 'logentry': { 'formatted': 'An error occurred while processing the request' }, 'level': 'error', }, id='log entry'), ] ) def test_before_send_with_frequency_limit(event): """Test before_send function frequency limit feature.""" TTL_SECONDS = 60 sentry_util.recently_sent_to_sentry.clear() with freezegun.freeze_time('2025-10-11 12:00:00') as frozen_time: # First call - should pass result = sentry_util.before_send(event, MagicMock()) assert result == event, 'First call - should pass' # Second immediate call - should skip result = sentry_util.before_send(event, MagicMock()) assert result is None, 'Second immediate call - should skip' # shift time ahead but still within TTL - still skip frozen_time.tick(delta=TTL_SECONDS - 1) result = sentry_util.before_send(event, MagicMock()) assert result is None, 'Within TTL - should skip still' # Now shift time beyond TTL - should pass again frozen_time.tick(delta=2) result = sentry_util.before_send(event, MagicMock()) assert result == event, 'After TTL - should pass again' @pytest.mark.parametrize( 'breadcrumb, expected_filtered_out', [ pytest.param( { 'category': 'test', 'message': 'Test breadcrumb', 'level': 'info', 'data': {'key': 'value'} }, False, id='standard breadcrumb' ), pytest.param( { 'category': 'test', 'message': 'Test breadcrumb with no data', 'level': 'info' }, False, id='breadcrumb without data' ), pytest.param( { 'category': 'httplib', 'type': 'http', 'data': { 'url': 'https://swf.example-amazon.com', 'method': 'GET', 'status_code': 200 }, }, True, id='SWF http request should be filtered out' ), pytest.param( { 'category': 'httplib', 'type': 'http', 'data': { 'aws.request.url': 'https://swf.example-amazon.com', 'aws.request.method': 'POST', 'status_code': 200 }, }, True, id='SWF http request should be filtered out' ), ] ) def test_before_breadcrumb(breadcrumb, expected_filtered_out): result = sentry_util.before_breadcrumb(breadcrumb, MagicMock()) if expected_filtered_out: assert result is None else: assert result == breadcrumb