"""Unit tests for Spotify Charts Weekly tasks workflow.""" from datetime import datetime from datetime import timedelta from unittest.mock import MagicMock from unittest.mock import patch import pytest from requests import HTTPError from feed_ingestion.flows.spotify_charts import config from feed_ingestion.flows.spotify_charts import tasks # Thursday _date = '2022-03-17' expected_countries = ['ad', 'ae', 'ar', 'us', 'global'] @pytest.fixture def expected_bootstrap_response(): """Response for bootstrap task.""" return { 'feed_name': 'spotify_charts_weekly_top_songs', 'date': '2022-03-17', 'chart': 'weekly_top_songs', 'staging_raw_table': 'staging_raw_spotify_charts', 'secrets_path': 'spotify_charts', 'archive_path': 'SpotifyCharts/archives/weekly_top_songs/2022-03-17/', 's3_archive_path': ('s3://dev-cucumbers/SpotifyCharts/archives/' 'weekly_top_songs/2022-03-17/'), 'file_pattern': 'regional_{country}_weekly_2022-03-17.json', 'temp_staging_raw_table': ( 'temp_staging_raw_spotify_charts_weekly_top_songs_' '{country}_20220317'), 'common_kwargs': { 'chart_type': 'regional', 'frequency': 'weekly', 'number_of_countries': 69}} @pytest.fixture def get_downloaded_files(): """Return downloaded_files.""" return { 'TikTok': { 'TopHashtag': 'TikTok_Trends_TopHashtag_20210103.txt', 'TopSong': 'TikTok_Trends_TopSong_20210103.txt'}, 'Douyin': { 'TopHashtag': 'Douyin_Trends_TopHashtag_20210103.txt', 'TopSong': 'Douyin_Trends_TopSong_20210103.txt'}} @pytest.fixture def mock_get_overall_status(): """Yield get overall status.""" overall_status_path = ( 'feed_ingestion.flows.tiktok_weekly.tasks.garcon_feed_status.' 'get_overall_status') with patch(overall_status_path) as overall_status: yield overall_status def test_bootstrap(expected_bootstrap_response, mock_get_overall_status): """Test bootstrap task with last day of a week.""" result = tasks.bootstrap( MagicMock(), _date, reload=False, chart='weekly_top_songs') assert result == expected_bootstrap_response def test_bootstrap_ingested_status(mock_get_overall_status): """Test bootstrap task if overall status is already INGESTED.""" mock_get_overall_status.return_value = 'INGESTED' result = tasks.bootstrap( MagicMock(), _date, reload=False, chart='weekly_top_songs') assert result == {'stop': True} def test_bootstrap_context_date_weekly_chart(mock_get_overall_status): """Test bootstrap task.""" # test with Friday context date date_obj = datetime.strptime(_date, '%Y-%m-%d') friday_date = (date_obj + timedelta(days=1)).strftime('%Y-%m-%d') result = tasks.bootstrap( MagicMock(), friday_date, reload=False, chart='weekly_top_songs') assert result['date'] == _date # test with Monday context date date_obj = datetime.strptime(_date, '%Y-%m-%d') print(date_obj) monday_date = (date_obj + timedelta(days=5)).strftime('%Y-%m-%d') result = tasks.bootstrap( MagicMock(), monday_date, reload=False, chart='weekly_top_songs') assert result['date'] == _date def test_bootstrap_context_date_daily_chart(mock_get_overall_status): """Test bootstrap task.""" # test with Friday context date date_obj = datetime.strptime(_date, '%Y-%m-%d') friday_date = (date_obj + timedelta(days=1)).strftime('%Y-%m-%d') result = tasks.bootstrap( MagicMock(), friday_date, reload=False, chart='daily_top_songs') assert result['date'] == friday_date # test with Monday context date date_obj = datetime.strptime(_date, '%Y-%m-%d') print(date_obj) monday_date = (date_obj + timedelta(days=5)).strftime('%Y-%m-%d') result = tasks.bootstrap( MagicMock(), monday_date, reload=False, chart='daily_top_songs') assert result['date'] == monday_date @pytest.fixture def mock_task_status(): """Yield task status.""" task_status_path = 'feed_ingestion.flows.spotify_charts.tasks.task_status' with patch(task_status_path) as task_status: task_status.get_values = MagicMock() task_status.set_values = MagicMock() yield task_status @pytest.fixture def mock_set_missing_files(): """Yield overall status.""" set_missing_files_path = ( 'feed_ingestion.flows.spotify_charts.tasks.garcon_feed_status.' 'set_missing_files') with patch(set_missing_files_path) as set_missing_files_status: yield set_missing_files_status @pytest.fixture def mock_set_overall_status(): """Yield overall status.""" overall_status_path = ( 'feed_ingestion.flows.spotify_charts.tasks.garcon_feed_status.' 'set_overall_status') with patch(overall_status_path) as overall_status: yield overall_status @pytest.fixture def mock_upload_on_s3(): """Yield upload_on_s3.""" path = 'feed_ingestion.flows.spotify_charts.tasks.upload_on_s3' with patch(path) as mock_s3: yield mock_s3 @pytest.fixture def mock_get_secret(): """Yield get_secret.""" path = 'feed_ingestion.flows.spotify_charts.tasks.get_secret' with patch(path) as mock_secret: mock_secret.return_value = 'secret' yield mock_secret @pytest.fixture def mock_spotify_api(): """Spotify API Wrapper fixture.""" sa_path = 'feed_ingestion.flows.spotify_charts.tasks.SpotifyChartsAPI' with patch(sa_path) as spotify_api: api_instance = spotify_api.return_value yield api_instance @pytest.fixture def mock_executor_context(): """Yield executor context.""" sf_executor_class_path = ( 'feed_ingestion.flows.spotify_charts.tasks.SpotifyCharts') with patch(sf_executor_class_path) as sf_executor: mock_executor_context = sf_executor.return_value.__enter__.return_value yield mock_executor_context @pytest.fixture def mock_spotify_api_file_not_available(): """Spotify API Wrapper fixture.""" sa_path = 'feed_ingestion.flows.spotify_charts.tasks.SpotifyChartsAPI' mock_response = MagicMock() mock_response.status_code = 404 err = HTTPError(response=mock_response) with patch(sa_path) as spotify_api: api_instance = spotify_api.return_value api_instance.get_charts_to_file = MagicMock(side_effect=err) yield api_instance @pytest.fixture def mock_temp_file(): """Yield temp_file.""" path = 'feed_ingestion.flows.spotify_charts.tasks.NamedTemporaryFile' with patch(path) as temp_file: mock_file = temp_file.return_value.__enter__.return_value yield mock_file def test_grab_drop_files_files_are_available( mock_task_status, mock_set_overall_status, mock_upload_on_s3, mock_get_secret, mock_spotify_api, mock_temp_file, mock_set_missing_files, monkeypatch): """Test test_grab_drop_files when all files are available.""" mock_task_status.get_values.return_value = [] feed_name = 'spotify_charts_daily_top_songs' monkeypatch.setattr(config, 'expected_countries', expected_countries) result = tasks.grab_drop_files( MagicMock(), feed_name=feed_name, date=_date, chart='daily_top_songs', file_pattern='regional_{country}_daily_2022-03-10.json', archive_path='SpotifyCharts/archives/daily_top_songs/2022-03-10/') mock_task_status.get_values.assert_called_with( feed_name, _date, 'ingested_countries_status') mock_set_overall_status.assert_called_with( feed_name, _date, 'DOWNLOADED') assert mock_spotify_api.get_charts_to_file.call_count \ == len(expected_countries) assert mock_upload_on_s3.call_count == len(expected_countries) mock_set_missing_files.assert_called_with( feed_name, _date, []) assert sorted(result['countries']) == sorted(expected_countries) def test_grab_drop_files_some_files_already_ingested( mock_task_status, mock_set_overall_status, mock_upload_on_s3, mock_get_secret, mock_spotify_api, mock_temp_file, mock_set_missing_files, monkeypatch): """Test test_grab_drop_files when all files are available.""" ingested_countries = ['ad', 'ae'] mock_task_status.get_values.return_value = ingested_countries feed_name = 'spotify_charts_daily_top_songs' monkeypatch.setattr(config, 'expected_countries', expected_countries) result = tasks.grab_drop_files( MagicMock(), feed_name=feed_name, date=_date, chart='daily_top_songs', file_pattern='regional_{country}_daily_2022-03-10.json', archive_path='SpotifyCharts/archives/daily_top_songs/2022-03-10/') mock_task_status.get_values.assert_called_with( feed_name, _date, 'ingested_countries_status') mock_set_overall_status.assert_called_with( feed_name, _date, 'DOWNLOADED') assert mock_spotify_api.get_charts_to_file.call_count \ == len(expected_countries) - len(ingested_countries) assert mock_upload_on_s3.call_count == \ len(expected_countries) - len(ingested_countries) mock_set_missing_files.assert_called_with( feed_name, _date, []) assert sorted(result['countries']) ==\ sorted(set(expected_countries) - set(ingested_countries)) def test_grab_drop_files_files_are_not_available( mock_task_status, mock_set_overall_status, mock_upload_on_s3, mock_get_secret, mock_spotify_api_file_not_available, mock_temp_file, monkeypatch, mock_set_missing_files): """Test test_grab_drop_files when all files are not available.""" mock_task_status.get_values.return_value = [] feed_name = 'spotify_charts_daily_top_songs' monkeypatch.setattr(config, 'expected_countries', expected_countries) result = tasks.grab_drop_files( MagicMock(), feed_name=feed_name, date=_date, chart='daily_top_songs', file_pattern='regional_{country}_daily_2022-03-10.json', archive_path='SpotifyCharts/archives/daily_top_songs/2022-03-10/') mock_task_status.get_values.assert_called_with( feed_name, _date, 'ingested_countries_status') mock_set_overall_status.assert_called_with( feed_name, _date, 'NOT_AVAILABLE') mock_upload_on_s3.assert_not_called() assert result == {'stop': True} def test_grab_drop_files_files_are_not_available_and_some_ingested( mock_task_status, mock_set_overall_status, mock_upload_on_s3, mock_get_secret, mock_spotify_api_file_not_available, mock_temp_file, monkeypatch, mock_set_missing_files): """Test test_grab_drop_files when all files are not available.""" ingested_countries = ['ad', 'ae'] mock_task_status.get_values.return_value = ingested_countries feed_name = 'spotify_charts_daily_top_songs' monkeypatch.setattr(config, 'expected_countries', expected_countries) result = tasks.grab_drop_files( MagicMock(), feed_name=feed_name, date=_date, chart='daily_top_songs', file_pattern='regional_{country}_daily_2022-03-10.json', archive_path='SpotifyCharts/archives/daily_top_songs/2022-03-10/') mock_task_status.get_values.assert_called_with( feed_name, _date, 'ingested_countries_status') mock_set_overall_status.assert_called_with( feed_name, _date, 'NOT_AVAILABLE') mock_upload_on_s3.assert_not_called() assert result == {'stop': True} def test_load_staging_charts_data(mock_executor_context): """Test load_staging_charts_data.""" for chart in config.charts: tasks.load_staging_charts_data( MagicMock(), 'feed_name', _date, chart, countries=expected_countries) mock_executor_context.clean_staging_raw_table.assert_called_once_with( staging_raw_table='spotify_chart', date=_date, chart_type=config.charts[chart]['chart_type'], frequency=config.charts[chart]['frequency'], countries=expected_countries) assert mock_executor_context.load_spotify_tables.call_count == len( config.spotify_metadata_tables) mock_executor_context.reset_mock() def test_set_status_to_ingested_all_files_ingested( mock_task_status, mock_set_overall_status, monkeypatch): """Test set_status_to_ingested.""" # there is no previously ingested countries mock_task_status.get_values.return_value = [] monkeypatch.setattr( config, 'expected_countries', expected_countries) monkeypatch.setattr( config, 'charts', { 'daily_top_songs': { 'chart_type': 'regional', 'frequency': 'daily', 'number_of_countries': len(expected_countries)}}) tasks.set_status_to_ingested( MagicMock(), 'feed_name', _date, expected_countries, 'daily_top_songs') assert sorted(mock_task_status.set_values.call_args_list[0][0][3]) == \ sorted(expected_countries) mock_set_overall_status.assert_called_with( 'feed_name', _date, 'INGESTED') def test_set_status_to_ingested_not_all_files_ingested( mock_task_status, mock_set_overall_status, monkeypatch): """Test set_status_to_ingested not all files ingested.""" # there is no previously ingested countries mock_task_status.get_values.return_value = [] monkeypatch.setattr( config, 'expected_countries', expected_countries) tasks.set_status_to_ingested( MagicMock(), 'feed_name', _date, ['ar', 'global'], 'daily_top_songs') assert sorted(mock_task_status.set_values.call_args_list[0][0][3]) == \ sorted(['ar', 'global']) mock_set_overall_status.assert_called_with( 'feed_name', _date, 'NOT_AVAILABLE') def test_set_status_to_ingested_if_some_files_were_previously_ingested( mock_task_status, mock_set_overall_status, monkeypatch): """Test set_status_to_ingested not all files ingested.""" mock_task_status.get_values.return_value = ['ar', 'global'] monkeypatch.setattr( config, 'expected_countries', expected_countries) tasks.set_status_to_ingested( MagicMock(), 'feed_name', _date, ['ad', 'ae'], 'daily_top_songs') assert sorted(mock_task_status.set_values.call_args_list[0][0][3]) ==\ sorted(['ad', 'ae', 'ar', 'global']) mock_set_overall_status.assert_called_with( 'feed_name', _date, 'NOT_AVAILABLE') @pytest.fixture def mock_jenkins(): """Mock jenkins.""" jenkins_path = 'feed_ingestion.flows.spotify_charts.tasks.jenkins' with patch(jenkins_path) as jenkins: jenkins.Jenkins.return_value = MagicMock() yield jenkins def test_build_jenkins_charts_without_build_charts(mock_jenkins): """Test build_jenkins_charts if build_charts is not 'True'.""" response = tasks.build_jenkins_charts( MagicMock(), 'feed_name', _date, 'chart', build_charts=None) mock_jenkins.assert_not_called() assert response == {'build': False} def test_build_jenkins_charts_with_build_charts(mock_jenkins, mock_get_secret): """Test build_jenkins_charts if build_charts is 'True'.""" mock_jenkins.Jenkins.return_value.get_job_info.return_value = { 'inQueue': False, 'nextBuildNumber': 1} response = tasks.build_jenkins_charts( MagicMock(), 'feed_name', _date, 'chart', build_charts='True') mock_jenkins.Jenkins.assert_called_with( config.jenkins_url, username='jenkinsjobrunner', password='secret') mock_jenkins.Jenkins.return_value.build_job.assert_called_with( config.jenkins_job, config.jenkins_job_params) mock_get_secret.assert_called_with( config.jenkins_secrets_path, 'JENKINS_API_TOKEN') assert response == {'build': True} def test_build_jenkins_charts_with_build_charts_with_queue( mock_jenkins, mock_get_secret): """Test build_jenkins_charts if there is a build in queue .""" mock_jenkins.Jenkins.return_value.get_job_info.return_value = { 'inQueue': True} response = tasks.build_jenkins_charts( MagicMock(), 'feed_name', _date, 'chart', build_charts='True') mock_jenkins.Jenkins.assert_called_with( config.jenkins_url, username='jenkinsjobrunner', password='secret') mock_jenkins.Jenkins.return_value.build_job.assert_not_called() mock_get_secret.assert_called_with( config.jenkins_secrets_path, 'JENKINS_API_TOKEN') assert response == {'build': False}