"""Unit tests for generic tasks of the FlowYouTubeMixin mixin.""" from unittest.mock import call, MagicMock, patch import boto3 from garcon_contrib.dynamo_feed_status import garcon_feed_status import moto import pytest from snowflake import connector from feed_ingestion import conf from feed_ingestion.flows.youtube_bulk_reports import config from feed_ingestion.tasks import s3_tasks from feed_ingestion.tasks import youtube_tasks from feed_ingestion.util import task_status from feed_ingestion.util.aws.athena import StatusError @moto.mock_aws def test_save_youtube_access_token_from_secrets(tmp_path): """Test save_youtube_access_token function.""" path = tmp_path / 'youtube_access_token.json' secrets_client = boto3.client('secretsmanager', region_name=conf.config.AWS_REGION) secrets_client.create_secret( Name='dev/swf-shared-youtube-access-token/YOUTUBE_ACCESS_TOKEN', SecretString='{"access_token": "test"}' ) youtube_tasks.save_youtube_access_token_from_secrets( path=path ) assert path.exists() assert path.read_text() == '{"access_token": "test"}' def test_update_channel_names_table(monkeypatch, sf_config_mock): """Test update_channel_names_table task.""" activity_mock = MagicMock() monkeypatch.setattr(task_status, 'is_completed_task', MagicMock( return_value=False)) monkeypatch.setattr(task_status, 'mark_completed_task', MagicMock()) connect_mock = MagicMock() monkeypatch.setattr(connector, 'connect', connect_mock) youtube_tasks.update_channel_names_table( activity_mock, '2020-06-01', 'youtube_claim_theorchard', sf_config_mock, secrets_path='youtube_claim') sql_from_call_insert = connect_mock.mock_calls[2][1][0] assert ('MERGE INTO test_db.test_schema.dim_youtube_channel_names' in sql_from_call_insert) @patch.object(youtube_tasks, 'athena') def test_sme_copy_from_athena_to_s3(athena_mock, monkeypatch): """Test SME copy from when Athena to S3.""" monkeypatch.setattr(task_status, 'is_completed_task', MagicMock( return_value=False)) remove_files_from_path_mock = MagicMock() monkeypatch.setattr(s3_tasks, 'remove_files_from_path', remove_files_from_path_mock) source_files_mock = MagicMock() source_files_mock.return_value = {'files': [{'file_name': 'file_name'}]} monkeypatch.setattr(s3_tasks, 'source_files', source_files_mock) set_overall_status_mock = MagicMock() monkeypatch.setattr(garcon_feed_status, 'set_overall_status', set_overall_status_mock) report_name = 'content_owner_ad_rates_a1' date = '2020-10-11' activity_mock = MagicMock() result = youtube_tasks.sme_copy_from_athena_to_s3( activity=activity_mock, date=date, report_status_name='sample_youtube_feed', sme_athena_database='sme_youtube_api_db', sme_athena_temp_database='test-temp', sme_athena_source_table=report_name, destination_s3_bucket='cucumbers', destination_s3_path=f'/YouTubeSME/efedorov/{date}/{report_name}/', athena_workgroup='test_workgroup', ) assert remove_files_from_path_mock.called assert source_files_mock.called assert set_overall_status_mock.called assert athena_mock.run_query.call_args_list == [ call( athena_query="SELECT * FROM sme_youtube_api_db.content_owner_ad_rates_a1\n WHERE CAST(report_date as date) = DATE('2020-10-11')\n AND report_licensor = 'sme'\n", # NOQA:E501 athena_temp_database='test-temp', athena_workgroup='test_workgroup', destination_s3_bucket='cucumbers', destination_s3_path=( '/YouTubeSME/efedorov/2020-10-11/content_owner_ad_rates_a1/'), timeout=60 * 60 ) ] assert result == {'files': [{'file_name': 'file_name'}]} @patch.object(youtube_tasks, 'athena') def test_sme_copy_from_athena_to_s3_athena_failure(athena_mock, monkeypatch): """Test SME copy from when Athena returns Failure for create table.""" monkeypatch.setattr(task_status, 'is_completed_task', MagicMock( return_value=False)) remove_files_from_path_mock = MagicMock() monkeypatch.setattr(s3_tasks, 'remove_files_from_path', remove_files_from_path_mock) monkeypatch.setattr(s3_tasks, 'source_files', MagicMock()) monkeypatch.setattr(garcon_feed_status, 'set_overall_status', MagicMock()) athena_mock.run_query.side_effect = StatusError report_name = 'content_owner_ad_rates_a1' date = '2020-10-11' activity_mock = MagicMock() with pytest.raises(StatusError): youtube_tasks.sme_copy_from_athena_to_s3( activity=activity_mock, date=date, report_status_name='sample_youtube_feed', sme_athena_database='sme_youtube_api_db', sme_athena_temp_database='test-temp', sme_athena_source_table=report_name, destination_s3_bucket='cucumbers', destination_s3_path=f'/YouTubeSME/efedorov/{date}/{report_name}/', athena_workgroup='test_workgroup', ) @patch.object(youtube_tasks, 'athena') def test_sme_copy_from_athena_to_s3_athena_not_available( athena_mock, monkeypatch): """Test SME copy from when Athena returns stop response.""" monkeypatch.setattr(task_status, 'is_completed_task', MagicMock( return_value=False)) remove_files_from_path_mock = MagicMock() monkeypatch.setattr(s3_tasks, 'remove_files_from_path', remove_files_from_path_mock) source_files_mock = MagicMock() source_files_mock.side_effect = ValueError monkeypatch.setattr(s3_tasks, 'source_files', source_files_mock) set_overall_status_mock = MagicMock() monkeypatch.setattr(garcon_feed_status, 'set_overall_status', set_overall_status_mock) report_name = 'content_owner_ad_rates_a1' date = '2020-10-11' activity_mock = MagicMock() result = youtube_tasks.sme_copy_from_athena_to_s3( activity=activity_mock, date=date, report_status_name='sample_youtube_feed', sme_athena_database='sme_youtube_api_db', sme_athena_temp_database='test-temp', sme_athena_source_table=report_name, destination_s3_bucket='cucumbers', destination_s3_path=f'/YouTubeSME/{date}/{report_name}/', athena_workgroup='test_workgroup', ) assert source_files_mock.called assert set_overall_status_mock.called assert result == {'stop': True, 'message': 'Athena data files not found'} @patch.object(youtube_tasks, 'athena') def test_sme_copy_from_athena_to_s3_exist_no_reload(athena_mock, monkeypatch): """Test SME copy from Athena when tasks was complited before.""" monkeypatch.setattr(task_status, 'is_completed_task', MagicMock( return_value=True)) remove_files_from_path_mock = MagicMock() monkeypatch.setattr(s3_tasks, 'remove_files_from_path', remove_files_from_path_mock) monkeypatch.setattr(s3_tasks, 'source_files', MagicMock()) monkeypatch.setattr(garcon_feed_status, 'set_overall_status', MagicMock()) report_name = 'content_owner_ad_rates_a1' date = '2020-10-11' activity_mock = MagicMock() youtube_tasks.sme_copy_from_athena_to_s3( activity=activity_mock, date=date, report_status_name='sample_youtube_feed', sme_athena_database='sme_youtube_api_db', sme_athena_temp_database='test-temp', sme_athena_source_table=report_name, destination_s3_bucket='cucumbers', destination_s3_path=f'/YouTubeSME/efedorov/{date}/{report_name}/', athena_workgroup='test_workgroup', ) assert not athena_mock.run_query.called @pytest.mark.skip(reason='Temporarily skipping these tests for POC') class BaseGrabReportsFiles(object): """Base class for grab_reports_files tests.""" @pytest.fixture def mock_task_status(self): """Yield task status.""" task_status_path = ( 'feed_ingestion.tasks.youtube_tasks.task_status') with patch(task_status_path) as task_status: task_status.is_completed_task.return_value = False task_status.mark_completed_task = MagicMock() yield task_status @pytest.fixture def mock_set_overall_status(self): """Yield overall status.""" overall_status_path = ( 'feed_ingestion.tasks.youtube_tasks.' 'garcon_feed_status.set_overall_status') with patch(overall_status_path) as overall_status: yield overall_status @pytest.fixture() def context_grab_reports_files(self): """Return context for grab_reports_files.""" return { 'activity': MagicMock(), 'date': '2018-04-01', 'archive_path': 's3://cucumbers/YouTubeBulkReports/' 'archives/2018-04-01/theorchard/', 'report_name': 'device_os', 'report_status_name': '{}_{}'.format( config.feed_name, 'device_os'), 'credentials_path': './', 'api_service_name': config.youtube_reporting_api_service_name, 'api_version': config.youtube_reporting_api_version, 'jobs_meta_path': config.jobs_meta_path, 'cms_dict': config.orchard_content_owners_map} @pytest.fixture def mock_get_authenticated_services(self): """Mock get_authenticated_services.""" path = ( 'feed_ingestion.util.' 'youtube_util.get_authenticated_services') with patch(path) as f: api_instance = f.return_value yield api_instance @pytest.fixture def mock_get_authenticated_services_fail(self): """Mock _get_authenticated_services failure.""" path = ( 'feed_ingestion.flows.util.' 'youtube_util.get_authenticated_services') with patch(path) as f: api_instance = f.return_value api_instance.jobs.return_value.reports. \ return_value.list.return_value.execute.return_value = None yield api_instance @pytest.fixture def mock_delete_obj(self): """Mock _get_authenticated_services failure.""" path = ( 'youtube_tasks.grab_reports_files.s3util.delete_s3_obj') with patch(path) as f: api_instance = f.return_value api_instance.jobs.return_value.reports. \ return_value.list.return_value.execute.return_value = None yield api_instance @pytest.fixture @patch.object(youtube_tasks.s3utils, 'delete_s3_obj') @patch.object(youtube_tasks.s3utils, 'upload_to_s3') @patch.object(youtube_tasks, 'sentry_util') def run_grab_reports_files( self, mock_sentry_util, mock_upload_to_s3, mock_delete_s3_obj, context_grab_reports_files, mock_get_authenticated_services, mock_set_overall_status, mock_task_status): """Run grab_reports_files.""" return youtube_tasks.grab_reports_files(**context_grab_reports_files) @pytest.mark.skip(reason='Temporarily skipping these tests for POC') class TestGrabReportsFiles(BaseGrabReportsFiles): """Test normal execution of grab_reports_files.""" @pytest.fixture def mock_next_chunk(self): """Mock MediaIoBaseDownload.next_chunk method.""" path = ( 'feed_ingestion.util.' 'youtube_util.MediaIoBaseDownload') with patch(path) as downloader: mock_downloader = MagicMock() mock_next_chunk = MagicMock(return_value=(True, True)) mock_downloader.next_chunk = mock_next_chunk downloader.return_value = mock_downloader yield mock_next_chunk def test_should_download_report( self, mock_next_chunk, run_grab_reports_files): """Should download report from YouTube API.""" assert mock_next_chunk.called def test_should_set_task_status( self, mock_next_chunk, mock_task_status, run_grab_reports_files): """Should set task status on success.""" assert mock_task_status.mark_completed_task.called @patch.object(youtube_tasks.s3utils, 'delete_s3_obj') @patch.object(youtube_tasks.s3utils, 'upload_to_s3') @patch.object(youtube_tasks.file_operations, 'split_file') @patch.object(youtube_tasks.youtube_util, 'grab_reports_files_for_content_owner') @patch.object(youtube_tasks.os, 'remove') def test_grab_reports_files_with_split( self, mock_remove, mock_grab_reports_files_for_content_owner, mock_split_file, mock_upload_to_s3, mock_delete_s3_obj, context_grab_reports_files, mock_get_authenticated_services, mock_set_overall_status, mock_task_status): """Run grab_reports_files.""" mock_grab_reports_files_for_content_owner.return_value = ( '/tmp', '/tmp/report.gz') context_grab_reports_files.update( split_file=True, split_path='s3://cucumbers/YouTubeBulkReports/temp/2018-04-01/', **context_grab_reports_files ) youtube_tasks.grab_reports_files( **context_grab_reports_files ) assert mock_split_file.called assert mock_remove.called class TestGrabReportsFilesYouTubeFailure(BaseGrabReportsFiles): """Test execution of grab_reports_files if YouTube API is failing.""" @pytest.fixture def mock_get_authenticated_services(self): """Mock get_authenticated_services failure.""" path = ( 'feed_ingestion.tasks.youtube_tasks.' 'youtube_util.get_authenticated_services') with patch(path) as f: api_instance = f.return_value api_instance.jobs.return_value.reports. \ return_value.list.return_value.execute.return_value = None yield api_instance def test_should_fail_if_report_is_not_available( self, context_grab_reports_files, mock_set_overall_status, mock_task_status, run_grab_reports_files): """Should set NOT_AVAILABLE status if report is not available.""" mock_set_overall_status.assert_called_with( context_grab_reports_files['report_status_name'], context_grab_reports_files['date'], garcon_feed_status.STATUS_NOT_AVAILABLE) assert not mock_task_status.mark_completed_task.called