"""Unit tests for tasks of iTunes Tickets Workflow.""" from importlib import reload from subprocess import CalledProcessError from unittest import mock from unittest.mock import MagicMock from unittest.mock import patch from boto3.exceptions import S3UploadFailedError from freezegun import freeze_time from garcon_contrib.dynamo_feed_status import garcon_feed_status import pytest from feed_ingestion import tasks as common_tasks from feed_ingestion.flows.itunes_tickets import config from feed_ingestion.flows.itunes_tickets import tasks _date = '2018-02-12' @pytest.fixture(autouse=True) def mock_check_status(request, monkeypatch): """Mock check_status decorator. Mock so that the decorated function is kept intact and always get called. """ mock = MagicMock() # just return the function without any modifications mock.return_value = lambda f: f monkeypatch.setattr(common_tasks, 'check_status', value=mock) reload(tasks) # redecorate tasks yield monkeypatch.undo() reload(tasks) @pytest.fixture def expected_bootstrap_response(): """Response for bootstrap task.""" return { 'feed_name': config.feed_name, 'secrets_path': config.secrets_path, 'archive_path': 'iTunesTickets/archives/{}/'.format(_date), 'preprocessed_path': 'iTunesTickets/preprocessed/{}/'.format(_date), 'temp_table_names': { 'ticket_notes': 'temp_itunes_ticket_notes_20180212', 'tickets': 'temp_itunes_tickets_20180212', 'ticket_defect_codes': 'temp_itunes_ticket_defect_codes_20180212'}, 'date': _date} @pytest.fixture def mock_set_overall_status(): """Yield overall status.""" overall_status_path = ( 'feed_ingestion.flows.itunes_tickets.tasks.garcon_feed_status.' 'set_overall_status') with patch(overall_status_path) as overall_status: yield overall_status @pytest.fixture def mock_get_overall_status(): """Yield overall status.""" overall_status_path = ( 'feed_ingestion.flows.itunes_tickets.tasks.garcon_feed_status.' 'get_overall_status') with patch(overall_status_path) as overall_status: yield overall_status def test_bootstrap(expected_bootstrap_response): """Bootstrap should return expected response.""" result = tasks.bootstrap(MagicMock(), date=_date, reload=False) assert result == expected_bootstrap_response class TestGrabDropFiles(object): """Test grab_drop_files.""" @pytest.fixture def mock_s3_utils(self): """Mock s3 utils.""" s3_utils_path = 'feed_ingestion.flows.itunes_tickets.tasks.s3' with patch(s3_utils_path) as s3_utils: mock_client = MagicMock() s3_utils.upload_on_s3 = mock_client yield s3_utils @pytest.fixture def mock_s3_utils_fail(self): """Mock s3 utils failure.""" s3_utils_path = 'feed_ingestion.flows.itunes_tickets.tasks.s3' with patch(s3_utils_path) as s3_utils: mock_client = MagicMock(side_effect=S3UploadFailedError()) s3_utils.upload_on_s3 = mock_client yield s3_utils @pytest.fixture def context_grab_drop_files(self): """Return context for grab_drop_files.""" return { 'activity': MagicMock(), 'feed_name': config.feed_name, 'date': _date, 'archive_path': 'iTunesTickets/archives/{}/'.format(_date) } @patch('feed_ingestion.flows.itunes_tickets.tasks.subprocess') @patch('feed_ingestion.flows.itunes_tickets.tasks.getsize') def test_grab_drop_files_success( self, mock_getsize, mock_subprocess, context_grab_drop_files, mock_s3_utils, mock_set_overall_status): """Test grab_drop_files successful scenario.""" tasks.grab_drop_files(**context_grab_drop_files) mock_s3_utils.upload_on_s3.assert_called_with( config.data_bucket, context_grab_drop_files['archive_path'], config.source_filename, mock.ANY, config.expected_bucket_owner) @patch('feed_ingestion.flows.itunes_tickets.tasks.subprocess') @patch('feed_ingestion.flows.itunes_tickets.tasks.getsize') def test_grab_drop_files_empty_file( self, mock_getsize, mock_subprocess, context_grab_drop_files, mock_s3_utils, mock_set_overall_status): """Test grab_drop_files returns stop when file is empty.""" mock_getsize.return_value = 0 result = tasks.grab_drop_files(**context_grab_drop_files) assert result['stop'] is True assert 'message' in result assert 'size is 0B' in result['message'] mock_set_overall_status.assert_called_with( config.feed_name, _date, garcon_feed_status.STATUS_NOT_AVAILABLE) @patch('feed_ingestion.flows.itunes_tickets.tasks.subprocess') def test_grab_drop_files_subprocess_fail( self, mock_subprocess, context_grab_drop_files, mock_s3_utils, mock_set_overall_status): """Test grab_drop_files returns stop when subprocess fails.""" mock_subprocess.run.side_effect = CalledProcessError( 1, 'cmd', 'output') result = tasks.grab_drop_files(**context_grab_drop_files) assert result['stop'] is True assert 'message' in result assert 'Transporter error' in result['message'] mock_set_overall_status.assert_called_with( config.feed_name, _date, garcon_feed_status.STATUS_NOT_AVAILABLE) @patch('feed_ingestion.flows.itunes_tickets.tasks.subprocess') @patch('feed_ingestion.flows.itunes_tickets.tasks.getsize') def test_grab_drop_files_fail( self, mock_getsize, mock_subprocess, context_grab_drop_files, mock_s3_utils_fail, mock_set_overall_status): """Test grab_drop_files returns stop when S3 upload fails.""" result = tasks.grab_drop_files(**context_grab_drop_files) assert result['stop'] is True assert 'message' in result assert 'Cannot upload file' in result['message'] mock_set_overall_status.assert_called_with( config.feed_name, _date, garcon_feed_status.STATUS_NOT_AVAILABLE) class TestProcessDropFiles(object): """Test process_drop_files.""" @pytest.fixture def mock_s3_get_source_files_content(self): """Mock get_source_files_content.""" path = ( 'feed_ingestion.flows.itunes_tickets' '.tasks.s3.get_source_files_content') with patch(path) as mock_s3_get_source_files_content: mock_s3_get_source_files_content.return_value = [ ('tickets.xml', '')] yield mock_s3_get_source_files_content @pytest.fixture def mock_get_tickets_and_notes(self): """Mock _get_tickets_and_notes.""" path = ( 'feed_ingestion.flows.itunes_tickets' '.tasks._get_tickets_and_notes') with patch(path) as mock_get_tickets_and_notes: mock_get_tickets_and_notes.return_value = ([], [], []) yield mock_get_tickets_and_notes @pytest.fixture def context(self): """Process drop files context.""" return { 'activity': MagicMock(), 'feed_name': config.feed_name, 'date': _date, 'archive_path': 'iTunesTickets/archives/{}/'.format(_date), 'preprocessed_path': 'iTunesTickets/preprocessed/{}/'.format(_date), 'source_files_dict': { 'files': ['tickets.xml'] } } @freeze_time('2019-03-01') def test_get_tickets_and_notes(self): """Test _get_tickets_and_notes.""" content = ( '' '' ' ' ' 111111' ' Audio File' ' 111' ' 1111' ' Song' ' English' ' R&B/Soul' ' Test1' ' 1' ' 7' ' ' 'The Orchard Enterprises Inc.' ' Apple' ' Your ' 'Action Needed' ' 2010-12-13T15:21:36-08:00' ' 2010-12-13T15:21:36-08:00' ' ' ' ' ' ' ' Static cuts through the audio ' 'at 0:00-0:18 elapsed time' ' 2010-12-13T15:21:36-08:00' ' ' ' ' ' ' ' ' ' 222222' ' Audio File' ' 222' ' 2222' ' 22222' ' Song' ' Jazz' ' Test 2' ' 1' ' 24' ' The Orchard ' 'Enterprises Inc.' ' Apple' ' ' 'Your Action Needed' ' 2011-01-28T15:55:32-08:00' ' 2011-01-28T15:55:32-08:00' ' ' ' Audio, Quality' ' ' ' ' ' ' ' This song ' 'skips at 0:14-0:26 elapsed time' ' 2011-01-28T15:55:32-08:00' ' ' ' ' ' ' '') tickets, notes, defect_codes = tasks._get_tickets_and_notes( content, _date) tickets_expected = [ { 'file_date': _date, 'ticketid': '111111', 'contenttickettype': 'Audio File', 'contentadamid': '111', 'contentvendorid': '1111', 'contenttype': 'Song', 'contentlanguage': 'English', 'contentgenre': 'R&B/Soul', 'name': 'Test1', 'discnumber': '1', 'tracknumber': '7', 'contentprovider': 'The Orchard Enterprises Inc.', 'openedby': 'Apple', 'contentticketstate': 'Your Action Needed', 'created': '2010-12-13T15:21:36-08:00', 'lastmodified': '2010-12-13T15:21:36-08:00', 'orchard_upc': '1111', 'contentupc': None }, { 'file_date': _date, 'ticketid': '222222', 'contenttickettype': 'Audio File', 'contentadamid': '222', 'contentvendorid': '2222', 'contenttype': 'Song', 'contentlanguage': None, 'contentgenre': 'Jazz', 'name': 'Test 2', 'discnumber': '1', 'tracknumber': '24', 'contentprovider': 'The Orchard Enterprises Inc.', 'openedby': 'Apple', 'contentticketstate': 'Your Action Needed', 'created': '2011-01-28T15:55:32-08:00', 'lastmodified': '2011-01-28T15:55:32-08:00', 'orchard_upc': '22222', 'contentupc': '22222' }, ] notes_expected = [ { 'file_date': _date, 'ticketid': '111111', 'note_text': 'Static cuts through the ' 'audio at 0:00-0:18 elapsed time', 'note_datetime': '2010-12-13T15:21:36-08:00', }, { 'file_date': _date, 'ticketid': '222222', 'note_text': 'This song skips at 0:14-0:26 elapsed time', 'note_datetime': '2011-01-28T15:55:32-08:00', } ] defect_codes_expected = [ { 'file_date': _date, 'ticketid': '222222', 'defect_code': 'Audio, Quality' } ] assert tickets == tickets_expected assert notes == notes_expected assert defect_codes == defect_codes_expected @patch('feed_ingestion.flows.itunes_tickets.tasks._create_csv') @patch('feed_ingestion.flows.itunes_tickets' '.tasks.s3.upload_processed_to_s3') def test_process_drop_files( self, mock_upload_processed_to_s3, mock_create_csv, mock_set_overall_status, mock_s3_get_source_files_content, mock_get_tickets_and_notes, context): """Test process_drop_files.""" tasks.process_drop_files(**context) s3_path = 's3://{}/{}'.format( config.data_bucket, context['archive_path']) mock_s3_get_source_files_content.assert_called_with( s3_path, context['source_files_dict']) assert mock_get_tickets_and_notes.called mock_create_csv.assert_has_calls([ mock.call(mock.ANY, config.ticket_fieldnames, '\t'), mock.call(mock.ANY, config.note_fieldnames, '\t') ]) tickets_upload_path = 's3://{}/{}{}'.format( config.data_bucket, context['preprocessed_path'], config.preprocessed_tickets_filename) notes_upload_path = 's3://{}/{}{}'.format( config.data_bucket, context['preprocessed_path'], config.preprocessed_notes_filename) mock_upload_processed_to_s3.assert_has_calls([ mock.call(mock.ANY, tickets_upload_path, expected_bucket_owner=config.expected_bucket_owner), mock.call(mock.ANY, notes_upload_path, expected_bucket_owner=config.expected_bucket_owner) ]) @patch('feed_ingestion.flows.itunes_tickets.tasks._create_csv') @patch('feed_ingestion.flows.itunes_tickets' '.tasks.s3.upload_processed_to_s3', side_effect=S3UploadFailedError) def test_process_drop_files_fail( self, mock_upload_processed_to_s3, mock_create_csv, mock_set_overall_status, mock_s3_get_source_files_content, mock_get_tickets_and_notes, context): """Test process_drop_files upload failure.""" with pytest.raises(S3UploadFailedError): tasks.process_drop_files(**context) mock_set_overall_status.assert_called_with( config.feed_name, _date, garcon_feed_status.STATUS_NOT_AVAILABLE)