"""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)