"""Unit tests for reusable Garcon tasks related to a feed's status.""" from unittest import mock from unittest.mock import patch import boto3 from garcon import activity from garcon_contrib.dynamo_feed_status import \ garcon_feed_status from feed_ingestion.tasks import feed_status_tasks @patch('feed_ingestion.tasks.feed_status_tasks.task_status') @patch('feed_ingestion.tasks.reporter_tasks.garcon_feed_status.' 'get_overall_status') @patch('feed_ingestion.tasks.reporter_tasks.garcon_feed_status.' 'set_overall_status') @patch('feed_ingestion.tasks.reporter_tasks.garcon_feed_status.' 'set_missing_files') def test_update_feed_file_status( mock_set_missing_files, mock_set_overall_status, mock_get_overall_status, mock_task_status): """Test update_feed_file_status.""" feed_name = 'FEED' date = '2016-01-01' files = ['file01', 'file02', 'file03'] s3_path = 's3://some_s3_path/' # test missing a file mock_task_status.get_item.return_value = { 'file01_status': garcon_feed_status.STATUS_DOWNLOADED, 'file02_status': garcon_feed_status.STATUS_DOWNLOADED, 'file03_status': garcon_feed_status.STATUS_NOT_AVAILABLE} context = feed_status_tasks.update_feed_file_status( activity.Activity(boto3.client('swf', 'us-east-1')), feed_name, date, files, s3_path) mock_task_status.get_item.assert_any_call(feed_name, date) mock_set_overall_status.assert_called_with( feed_name, date, garcon_feed_status.STATUS_NOT_AVAILABLE, attributes={'updated_at': mock.ANY}) mock_set_missing_files.assert_called_with( feed_name, date, ['s3://some_s3_path/file03']) assert context == {'file_status': garcon_feed_status.STATUS_NOT_AVAILABLE} mock_task_status.mark_completed_task.assert_not_called() @patch('feed_ingestion.tasks.feed_status_tasks.task_status') @patch('feed_ingestion.tasks.reporter_tasks.garcon_feed_status.' 'get_overall_status') @patch('feed_ingestion.tasks.reporter_tasks.garcon_feed_status.' 'set_overall_status') @patch('feed_ingestion.tasks.reporter_tasks.garcon_feed_status.' 'set_missing_files') def test_update_feed_file_status_2( mock_set_missing_files, mock_set_overall_status, mock_get_overall_status, mock_task_status): """Test update_feed_file_status.""" feed_name = 'FEED' date = '2016-01-01' files = ['file01', 'file02', 'file03'] s3_path = 's3://some_s3_path/' # test all files downloaded mock_task_status.get_item.return_value = { 'file01_status': garcon_feed_status.STATUS_DOWNLOADED, 'file02_status': garcon_feed_status.STATUS_DOWNLOADED, 'file03_status': garcon_feed_status.STATUS_DOWNLOADED} mock_get_overall_status.return_value = False context = feed_status_tasks.update_feed_file_status( activity.Activity(boto3.client('swf', 'us-east-1')), feed_name, date, files, s3_path) assert context == {'file_status': garcon_feed_status.STATUS_DOWNLOADED} mock_set_overall_status.assert_called_with( feed_name, date, garcon_feed_status.STATUS_DOWNLOADED, attributes={'updated_at': mock.ANY}) mock_task_status.mark_completed_task.assert_called_with( feed_name, date, 'reporter_to_s3') mock_set_missing_files.assert_not_called() @patch('feed_ingestion.tasks.feed_status_tasks.task_status') @patch('feed_ingestion.tasks.reporter_tasks.garcon_feed_status.' 'get_overall_status') @patch('feed_ingestion.tasks.reporter_tasks.garcon_feed_status.' 'set_overall_status') @patch('feed_ingestion.tasks.reporter_tasks.garcon_feed_status.' 'set_missing_files') def test_update_feed_file_status_3( mock_set_missing_files, mock_set_overall_status, mock_get_overall_status, mock_task_status): """Test update_feed_file_status.""" feed_name = 'FEED' date = '2016-01-01' files = ['file01'] s3_path = 's3://some_s3_path/' # test overall status updated when current value is NOT AVAILABLE mock_set_missing_files.reset_mock() mock_task_status.get_item.return_value = { 'file01_status': garcon_feed_status.STATUS_DOWNLOADED, 'file02_status': garcon_feed_status.STATUS_DOWNLOADED, 'file03_status': garcon_feed_status.STATUS_DOWNLOADED} mock_set_overall_status.reset_mock() mock_get_overall_status.return_value = \ garcon_feed_status.STATUS_NOT_AVAILABLE context = feed_status_tasks.update_feed_file_status( activity.Activity(boto3.client('swf', 'us-east-1')), feed_name, date, files, s3_path) mock_set_overall_status.assert_called_with( feed_name, date, garcon_feed_status.STATUS_DOWNLOADED, attributes={'updated_at': mock.ANY}) assert context == {'file_status': garcon_feed_status.STATUS_DOWNLOADED} mock_task_status.mark_completed_task.assert_called_with( feed_name, date, 'reporter_to_s3') @patch('feed_ingestion.tasks.feed_status_tasks.task_status') @patch('feed_ingestion.tasks.reporter_tasks.garcon_feed_status.' 'get_overall_status') @patch('feed_ingestion.tasks.reporter_tasks.garcon_feed_status.' 'set_overall_status') @patch('feed_ingestion.tasks.reporter_tasks.garcon_feed_status.' 'set_missing_files') def test_update_feed_file_status_4( mock_set_missing_files, mock_set_overall_status, mock_get_overall_status, mock_task_status): """Test update_feed_file_status.""" feed_name = 'FEED' date = '2016-01-01' files = ['file01'] s3_path = 's3://some_s3_path/' # test overall status not updated when not False or DOWNLOADED mock_task_status.get_item.return_value = { 'file01_status': garcon_feed_status.STATUS_DOWNLOADED, 'file02_status': garcon_feed_status.STATUS_DOWNLOADED, 'file03_status': garcon_feed_status.STATUS_DOWNLOADED} mock_task_status.get_newcontexts.return_value = set() mock_get_overall_status.return_value = garcon_feed_status.STATUS_INGESTED context = feed_status_tasks.update_feed_file_status( activity.Activity(boto3.client('swf', 'us-east-1')), feed_name, date, files, s3_path) assert mock_set_overall_status.call_count == 0 assert context == {'file_status': garcon_feed_status.STATUS_DOWNLOADED} mock_task_status.mark_completed_task.assert_not_called() @patch('feed_ingestion.tasks.feed_status_tasks.task_status') @patch('feed_ingestion.tasks.reporter_tasks.garcon_feed_status.' 'get_overall_status') @patch('feed_ingestion.tasks.reporter_tasks.garcon_feed_status.' 'set_overall_status') @patch('feed_ingestion.tasks.reporter_tasks.garcon_feed_status.' 'set_missing_files') def test_update_feed_file_status_5( mock_set_missing_files, mock_set_overall_status, mock_get_overall_status, mock_task_status): """Test overall status is STATUS_NOT_AVAILABLE for missing flies.""" feed_name = 'FEED' date = '2016-01-01' files = ['file01', 'file02', 'file03'] s3_path = 's3://some_s3_path/' mock_set_overall_status.reset_mock() mock_task_status.reset_mock() mock_task_status.get_item.return_value = { 'file01_status': garcon_feed_status.STATUS_DOWNLOADED, 'file02_status': garcon_feed_status.STATUS_NOT_AVAILABLE, 'file03_status': garcon_feed_status.STATUS_NOT_AVAILABLE} mock_get_overall_status.return_value = False context = feed_status_tasks.update_feed_file_status( activity.Activity(boto3.client('swf', 'us-east-1')), feed_name, date, files, s3_path, 'report_type', contexts_config={ 'default': { 'default': { 'optional': ['03'] } } }) mock_task_status.get_item.assert_any_call(feed_name, date) mock_set_overall_status.assert_called_with( feed_name, date, garcon_feed_status.STATUS_NOT_AVAILABLE, attributes={'updated_at': mock.ANY}) mock_set_missing_files.assert_called_with( feed_name, date, ['s3://some_s3_path/file02', 's3://some_s3_path/file03']) assert context == {'file_status': garcon_feed_status.STATUS_NOT_AVAILABLE} mock_task_status.mark_completed_task.assert_not_called() @patch('feed_ingestion.tasks.feed_status_tasks.task_status') @patch('feed_ingestion.tasks.reporter_tasks.garcon_feed_status.' 'get_overall_status') @patch('feed_ingestion.tasks.reporter_tasks.garcon_feed_status.' 'set_overall_status') @patch('feed_ingestion.tasks.reporter_tasks.garcon_feed_status.' 'set_missing_files') def test_update_feed_file_status_is_DOWNLOADED_if_optional_files_are_missing( mock_set_missing_files, mock_set_overall_status, mock_get_overall_status, mock_task_status): """Test update_feed_file_status.""" feed_name = 'FEED' date = '2016-01-01' files = ['file01', 'file02', 'file03'] s3_path = 's3://some_s3_path/' # test overall status is DOWNLOADED if only optional files are missing contexts_config = { 'default': { 'optional': ['03'] } } mock_task_status.get_item.return_value = { 'file01_status': garcon_feed_status.STATUS_DOWNLOADED, 'file02_status': garcon_feed_status.STATUS_DOWNLOADED, 'file03_status': garcon_feed_status.STATUS_NOT_AVAILABLE} mock_get_overall_status.return_value = False context = feed_status_tasks.update_feed_file_status( activity.Activity(boto3.client('swf', 'us-east-1')), feed_name, date, files, s3_path, 'report_type', contexts_config) mock_task_status.get_item.assert_any_call(feed_name, date) mock_set_overall_status.assert_called_with( feed_name, date, garcon_feed_status.STATUS_DOWNLOADED, attributes={'updated_at': mock.ANY}) mock_set_missing_files.assert_called_with( feed_name, date, ['s3://some_s3_path/file03']) assert context == {'file_status': garcon_feed_status.STATUS_DOWNLOADED} mock_task_status.mark_completed_task.assert_called_with( feed_name, date, 'reporter_to_s3') @patch('feed_ingestion.tasks.feed_status_tasks.garcon_feed_status.' 'set_overall_status') @patch('feed_ingestion.tasks.feed_status_tasks.garcon_feed_status.' 'set_status') @patch('feed_ingestion.tasks.feed_status_tasks.helpers') @patch('feed_ingestion.tasks.feed_status_tasks.garcon_feed_status.' 'set_missing_files') def test_update_feed_s3_file_status( mock_set_missing_files, mock_helper, mock_set_status, mock_set_overall_status): """Test update_feed_s3_file_status.""" feed = 'FEED' date = '2016-01-31' files = ['file01', 'file02', 'file03'] mock_helper.check_s3_key_exist.return_value = True response = feed_status_tasks.update_feed_s3_file_status( activity.Activity(boto3.client('swf', 'us-east-1')), 's3://TestCucumber/archive', files) assert mock_set_status.call_count == 3 assert response == {'file_status': 'DOWNLOADED'} mock_helper.check_s3_key_exist.return_value = False response = feed_status_tasks.update_feed_s3_file_status( activity.Activity(boto3.client('swf', 'us-east-1')), 's3://TestCucumber/archive', files, feed, date) mock_set_overall_status.assert_called_with( feed, date, garcon_feed_status.STATUS_NOT_AVAILABLE, attributes={'updated_at': mock.ANY}) assert response == { 'file_status': 'NOT_AVAILABLE', 'missing_files': 's3://TestCucumber/archivefile01,s3://TestCucumber/archivefile02,' 's3://TestCucumber/archivefile03'} @patch('feed_ingestion.tasks.feed_status_tasks.task_status') @patch('feed_ingestion.tasks.feed_status_tasks.s3utils') def test_check_files_on_s3_if_there_are_new_files( mock_s3utils, mock_task_status): """Test check_files_on_s3.""" mock_s3utils.get_list_of_files_and_directories.return_value = [ 's3://some_path/US test_pattern_us.csv', 's3://some_path/DE test_pattern_de.csv', 's3://some_path/US another_pattern_us.csv'] mock_task_status.get_values.return_value = [] expected_response = dict( source_files_dict={ 'files': [ {'file_name': 'US test_pattern_us.csv', 'found': True}, {'file_name': 'DE test_pattern_de.csv', 'found': True} ]}) result = feed_status_tasks.check_files_on_s3( activity=activity.Activity(boto3.client('swf', 'us-east-1')), feed_name='feed_name', date='2016-01-01', s3_download_path='test_path', file_pattern='.+test_pattern.+') assert result == expected_response @patch('feed_ingestion.tasks.feed_status_tasks.task_status') @patch('feed_ingestion.tasks.feed_status_tasks.s3utils') def test_check_files_on_s3_if_there_are_new_files_list_format( mock_s3utils, mock_task_status): """Test check_files_on_s3 with list_output.""" mock_s3utils.get_list_of_files_and_directories.return_value = [ 's3://some_path/US test_pattern_us.csv', 's3://some_path/DE test_pattern_de.csv', 's3://some_path/US another_pattern_us.csv'] mock_task_status.get_values.return_value = [] expected_response = dict( source_files_dict={ 'files': ['US test_pattern_us.csv', 'DE test_pattern_de.csv']}) result = feed_status_tasks.check_files_on_s3( activity=activity.Activity(boto3.client('swf', 'us-east-1')), feed_name='feed_name', date='2016-01-01', s3_download_path='test_path', file_pattern='.+test_pattern.+', list_output=True) assert result == expected_response @patch('feed_ingestion.tasks.feed_status_tasks.task_status') @patch('feed_ingestion.tasks.feed_status_tasks.s3utils') @patch('feed_ingestion.tasks.reporter_tasks.garcon_feed_status.' 'set_overall_status') def test_check_files_on_s3_if_there_are_not_new_files( mock_s3utils, mock_task_status, mock_set_overall_status): """Test check_files_on_s3.""" mock_s3utils.get_list_of_files_and_directories.return_value = [ 's3://some_path/US test_pattern_us.csv', 's3://some_path/DE test_pattern_de.csv', 's3://some_path/US another_pattern_us.csv'] mock_task_status.get_values.return_value = [ 'US test_pattern_us.csv', 'DE test_pattern_de.csv'] result = feed_status_tasks.check_files_on_s3( activity=activity.Activity(boto3.client('swf', 'us-east-1')), feed_name='feed_name', date='2016-01-01', s3_download_path='test_path', file_pattern='test_pattern') assert result == {'stop': True} @patch('feed_ingestion.tasks.feed_status_tasks.task_status') def test_mark_ingested_files(mock_task_status): """Test mark_ingested_files.""" source_files_dict = { 'files': [ {'file_name': 'DE test_pattern_de.csv', 'found': True, 'file_size': 1}, {'file_name': 'US test_pattern_us.csv', 'found': True, 'file_size': 1}]} feed_status_tasks.mark_ingested_files( activity=activity.Activity(boto3.client('swf', 'us-east-1')), feed_name='feed_name', date='2016-01-01', source_files_dict=source_files_dict) mock_task_status.set_values.assert_called_once_with( 'feed_name', '2016-01-01', 'ingested_files', ['DE test_pattern_de.csv', 'US test_pattern_us.csv']) @patch('feed_ingestion.tasks.feed_status_tasks.task_status') def test_mark_ingested_files_if_not_all_files_are_found(mock_task_status): """Test mark_ingested_files.""" source_files_dict = { 'files': [ {'file_name': 'DE test_pattern_de.csv', 'found': True, 'file_size': 1}, {'file_name': 'US test_pattern_us.csv', 'found': False, 'file_size': 1}]} feed_status_tasks.mark_ingested_files( activity=activity.Activity(boto3.client('swf', 'us-east-1')), feed_name='feed_name', date='2016-01-01', source_files_dict=source_files_dict) mock_task_status.set_values.assert_called_once_with( 'feed_name', '2016-01-01', 'ingested_files', ['DE test_pattern_de.csv']) @patch('feed_ingestion.tasks.feed_status_tasks.task_status') def test_mark_ingested_files_list_input(mock_task_status): """Test mark_ingested_files with list formats.""" source_files_dict_list = { 'files': ['DE test_pattern_de.csv', 'US test_pattern_us.csv'] } feed_status_tasks.mark_ingested_files( activity=activity.Activity(boto3.client('swf', 'us-east-1')), feed_name='feed_name', date='2016-01-01', source_files_dict=source_files_dict_list) mock_task_status.set_values.assert_called_with( 'feed_name', '2016-01-01', 'ingested_files', ['DE test_pattern_de.csv', 'US test_pattern_us.csv']) @patch('feed_ingestion.tasks.feed_status_tasks.task_status') def test_mark_ingested_files_empty_list(mock_task_status): """Test mark_ingested_files with list formats.""" source_files_dict_empty = {'files': []} feed_status_tasks.mark_ingested_files( activity=activity.Activity(boto3.client('swf', 'us-east-1')), feed_name='feed_name', date='2016-01-01', source_files_dict=source_files_dict_empty) mock_task_status.set_values.assert_called_with( 'feed_name', '2016-01-01', 'ingested_files', []) def test_get_contexts_config_for_report_1(): contexts_config = { 'default': { 'default': { 'required': ['1', '2', '3'] } } } optional, contexts = feed_status_tasks.get_contexts_config_for_report( 'amContent', contexts_config) assert optional is False assert contexts == ['1', '2', '3'] def test_get_contexts_config_for_report_2(): contexts_config = { 'default': { 'default': { 'optional': ['1', '2', '3'] } } } optional, contexts = feed_status_tasks.get_contexts_config_for_report( 'amContent', contexts_config) assert optional is True assert contexts == ['1', '2', '3'] def test_get_contexts_config_for_report_3(): contexts_config = { 'default': { 'default': { 'optional': ['1', '2', '3'] }, 'amContent': { 'required': ['1'] } } } optional, contexts = feed_status_tasks.get_contexts_config_for_report( 'amContent', contexts_config) assert optional is False assert contexts == ['1'] def test_get_contexts_config_for_report_4(): contexts_config = {} optional, contexts = feed_status_tasks.get_contexts_config_for_report( 'amContent', contexts_config) assert optional is False assert contexts == [] def test_file_is_optional_1(): contexts_config = { 'default': { 'optional': ['1', '2', '3'] } } optional = feed_status_tasks.file_is_optional( 'amContent', contexts_config, 'a_1_.gz') assert optional is True def test_file_is_optional_2(): contexts_config = { 'default': { 'default': { 'optional': ['1', '2', '3'] } } } optional = feed_status_tasks.file_is_optional( 'amContent', contexts_config, 'a_4_.gz') assert optional is False def test_file_is_optional_3(): contexts_config = { 'default': { 'default': { 'required': ['1', '2', '3'] } } } optional = feed_status_tasks.file_is_optional( 'amContent', contexts_config, 'a_1_.gz') assert optional is False def test_file_is_optional_4(): contexts_config = { 'default': { 'required': ['1', '2', '3'] } } optional = feed_status_tasks.file_is_optional( 'amContent', contexts_config, 'a_4_.gz') assert optional is True @patch('feed_ingestion.tasks.feed_status_tasks.boto3') def test_has_newer_ingested_date_true(mock_boto3): """A newer INGESTED row makes the guard report True.""" table = mock.MagicMock() table.query.return_value = {'Items': [{'date': '2026-06-22'}]} mock_boto3.resource.return_value.Table.return_value = table result = feed_status_tasks.has_newer_ingested_date( 'youtube_asset_conflict', '2026-06-21') assert result is True @patch('feed_ingestion.tasks.feed_status_tasks.boto3') def test_has_newer_ingested_date_false(mock_boto3): """No newer INGESTED row and no further pages reports False.""" table = mock.MagicMock() table.query.return_value = {'Items': []} mock_boto3.resource.return_value.Table.return_value = table result = feed_status_tasks.has_newer_ingested_date( 'youtube_asset_conflict', '2026-06-21') assert result is False