"""Lambda test module.""" import datetime import importlib from unittest import mock from unittest.mock import call from unittest.mock import patch from unittest.mock import PropertyMock import pytest from freezegun import freeze_time from freezegun.api import FakeDatetime # type: ignore[attr-defined] from soundrecording_utils.constants.ddex.constants import RuleService from soundrecording_utils.metadata.types import Asset from soundrecording_utils.metadata.types import OrchardSoundRecording from soundrecording_utils.metadata.types import TrackConnection from src import app as index @patch('src.app.s3_delivery_audit.write_sr_delivery_audit_xml') @patch('src.app.s3_asset_delivery.write_file') @patch('src.app.s3_sound_recordings.get_latest_sound_recording_version') @patch('src.app.generate_ddex') @patch('src.app.s3_assets.download_asset') @patch('src.app._generate_batch_id') @patch('src.app._s3_remote_foldernames') @freeze_time('2019-06-15 12:00:00') def test_handler_with_asset( mock_s3_foldernames, mock_batch_id, mock_download, mock_generate_ddex, mock_s3_get, mock_write_to_s3, mock_write_to_s3_audit, mock_asset): """Test handler function with upload of asset.""" mock_s3_foldernames.return_value = ( 'batch123', 'batch123/isrc-123', 'batch123/isrc-123/resources' ) mock_batch_id.return_value = 'batch123' mock_download.return_value = b'music' mock_s3_get.return_value = ( 'dummy OSR ID', """ { "track_connection": { "tracks": [] }, "assets": [{ "uuid": "b3955ca9-bfb0-4d09-8f18-9eb4fd8737ff", "filename": "90e575bd-94a5-4a3e-9f1f-dade36feb8c3", "extension": "flac" }], "isrc": "isrc-123" } """, False ) mock_generate_ddex.return_value = 'XML Result' input_event = { 'sound_recording': { 'id': 'sr-123' }, 'upload_asset': True, 'execution_name': '6ff851fa-d83b-4e6e-9888-66077de9529a' } result = index.handler(input_event, None) assert result == { 'details': { 'batch_id': 'batch123', 'filenames': [ 'batch123/isrc-123/resources/90e575bd-94a5-4a3e-9f1f-dade36feb8c3.flac', # noqa:E501 'batch123/isrc-123/isrc-123.xml', 'batch123/BatchComplete_batch123.xml' ] } } assert mock_write_to_s3_audit.call_args_list == [ call( b'XML Result', 'batch123/isrc-123/isrc-123.xml', '6ff851fa-d83b-4e6e-9888-66077de9529a' ) ] assert mock_write_to_s3.call_args_list == [ call(b'XML Result', 'batch123/isrc-123/isrc-123.xml', '6ff851fa-d83b-4e6e-9888-66077de9529a'), call(b'music', 'batch123/isrc-123/resources/90e575bd-94a5-4a3e-9f1f-dade36feb8c3.flac', # noqa:E501 '6ff851fa-d83b-4e6e-9888-66077de9529a'), call(b'', 'batch123/BatchComplete_batch123.xml', '6ff851fa-d83b-4e6e-9888-66077de9529a'), ] assert mock_s3_get.call_args_list == [ call('sr-123') ] assert mock_generate_ddex.call_args_list == [ call( OrchardSoundRecording( track_connection=TrackConnection(tracks=[]), assets=[Asset(uuid='b3955ca9-bfb0-4d09-8f18-9eb4fd8737ff', filename='90e575bd-94a5-4a3e-9f1f-dade36feb8c3', extension='flac')], # noqa:E501 isrc='isrc-123' ), mock_asset, { 'timestamp': FakeDatetime(2019, 6, 15, 12, 0, tzinfo=datetime.timezone.utc), # noqa:E501 'message_thread_id': 1, 'message_id': 'batch123' }, 'lambda-sr-delivery-tiktok', RuleService.TIKTOK, selected_version=index.config.DDEX_FILE_VERSION, takedown=False ) ] assert mock_s3_foldernames.call_args_list == [ call('batch123', 'isrc-123')] @patch('src.app.s3_delivery_audit.write_sr_delivery_audit_xml') @patch('src.app.s3_asset_delivery.write_file') @patch('src.app.s3_sound_recordings.get_sound_recording_version') @patch('src.app.generate_ddex') @patch('src.app.s3_assets.download_asset') @patch('src.app._generate_batch_id') @patch('src.app._s3_remote_foldernames') @freeze_time('2019-06-15 12:00:00') def test_handler_with_asset_and_sr_version( mock_s3_foldernames, mock_batch_id, mock_download, mock_generate_ddex, mock_s3_get, mock_write_to_s3, mock_write_to_s3_audit, mock_asset): """Test handler function with upload of asset.""" mock_s3_foldernames.return_value = ( 'batch123', 'batch123/isrc-123', 'batch123/isrc-123/resources' ) mock_batch_id.return_value = 'batch123' mock_download.return_value = b'music' mock_s3_get.return_value = ( """ { "track_connection": { "tracks": [] }, "assets": [{ "uuid": "b3955ca9-bfb0-4d09-8f18-9eb4fd8737ff", "filename": "90e575bd-94a5-4a3e-9f1f-dade36feb8c3", "extension": "flac" }], "isrc": "isrc-123" } """, 'blerg' ) mock_generate_ddex.return_value = 'XML Result' input_event = { 'sound_recording': { 'id': 'sr-123', 'version': 'v-123' }, 'upload_asset': True, 'execution_name': '6ff851fa-d83b-4e6e-9888-66077de9529a' } result = index.handler(input_event, None) assert result == { 'details': { 'batch_id': 'batch123', 'filenames': [ 'batch123/isrc-123/resources/90e575bd-94a5-4a3e-9f1f-dade36feb8c3.flac', 'batch123/isrc-123/isrc-123.xml', 'batch123/BatchComplete_batch123.xml' ] } } assert mock_write_to_s3_audit.call_args_list == [ call( b'XML Result', 'batch123/isrc-123/isrc-123.xml', '6ff851fa-d83b-4e6e-9888-66077de9529a' ) ] assert mock_write_to_s3.call_args_list == [ call(b'XML Result', 'batch123/isrc-123/isrc-123.xml', '6ff851fa-d83b-4e6e-9888-66077de9529a'), call(b'music', 'batch123/isrc-123/resources/90e575bd-94a5-4a3e-9f1f-dade36feb8c3.flac', # noqa:E501 '6ff851fa-d83b-4e6e-9888-66077de9529a'), call(b'', 'batch123/BatchComplete_batch123.xml', '6ff851fa-d83b-4e6e-9888-66077de9529a'), ] assert mock_s3_get.call_args_list == [ call('sr-123', 'v-123') ] assert mock_generate_ddex.call_args_list == [ call( OrchardSoundRecording( track_connection=TrackConnection(tracks=[]), assets=[Asset(uuid='b3955ca9-bfb0-4d09-8f18-9eb4fd8737ff', filename='90e575bd-94a5-4a3e-9f1f-dade36feb8c3', extension='flac')], # noqa:E501, isrc='isrc-123' ), mock_asset, { 'timestamp': FakeDatetime(2019, 6, 15, 12, 0, tzinfo=datetime.timezone.utc), # noqa:E501 'message_thread_id': 1, 'message_id': 'batch123' }, 'lambda-sr-delivery-tiktok', RuleService.TIKTOK, selected_version=index.config.DDEX_FILE_VERSION, takedown=False ) ] assert mock_s3_foldernames.call_args_list == [ call('batch123', 'isrc-123')] @patch('src.app.s3_delivery_audit.write_sr_delivery_audit_xml') @patch('src.app.s3_asset_delivery.write_file') @patch('src.app.s3_sound_recordings.get_latest_sound_recording_version') @patch('src.app.generate_ddex') @patch('src.app.s3_assets.download_asset') @patch('src.app._generate_batch_id') @patch('src.app._s3_remote_foldernames') @freeze_time('2019-06-15 12:00:00') def test_handler_without_asset( mock_s3_foldernames, mock_batch_id, mock_download, mock_generate_ddex, mock_s3_get, mock_write_to_s3, mock_write_to_s3_audit, mock_asset): """Test handler function without upload of asset.""" mock_s3_foldernames.return_value = ( 'batch123', 'batch123/isrc-123', 'batch123/isrc-123/resources' ) mock_batch_id.return_value = 'batch123' mock_s3_get.return_value = ( 'dummy OSR ID', """ { "track_connection": { "tracks": [] }, "assets": [], "isrc": "isrc-123" } """, False ) mock_generate_ddex.return_value = 'XML Result' input_event = { 'sound_recording': { 'id': 'sr-123' }, 'upload_asset': False, 'execution_name': '6ff851fa-d83b-4e6e-9888-66077de9529a' } result = index.handler(input_event, None) assert result == { 'details': { 'batch_id': 'batch123', 'filenames': [ 'batch123/isrc-123/isrc-123.xml', 'batch123/BatchComplete_batch123.xml' ] } } assert mock_s3_get.called assert not mock_download.called assert mock_write_to_s3_audit.call_args_list == [ call( b'XML Result', 'batch123/isrc-123/isrc-123.xml', '6ff851fa-d83b-4e6e-9888-66077de9529a' )] assert mock_write_to_s3.call_args_list == [ call(b'XML Result', 'batch123/isrc-123/isrc-123.xml', '6ff851fa-d83b-4e6e-9888-66077de9529a'), call(b'', 'batch123/BatchComplete_batch123.xml', '6ff851fa-d83b-4e6e-9888-66077de9529a'), ] assert mock_generate_ddex.call_args_list == [ call( OrchardSoundRecording( track_connection=TrackConnection(tracks=[]), assets=[], isrc='isrc-123' ), None, { 'timestamp': FakeDatetime(2019, 6, 15, 12, 0, tzinfo=datetime.timezone.utc), # noqa:E501 'message_thread_id': 1, 'message_id': 'batch123' }, 'lambda-sr-delivery-tiktok', RuleService.TIKTOK, selected_version=index.config.DDEX_FILE_VERSION, takedown=False ) ] assert mock_s3_foldernames.call_args_list == [ call('batch123', 'isrc-123')] @patch( 'src.app.config.TIKTOK_S3_BASE_FOLDERNAME', new_callable=PropertyMock(return_value='') ) @patch( 'src.app.config.RESOURCES_DIRNAME', new_callable=PropertyMock(return_value='resources') ) def test_s3_remote_foldernames_default(mock_resources_dir, mock_base_dir): """Test default remote dirnames.""" (batch_dir, release_dir, resources_dir) = index._s3_remote_foldernames( 'batch123', 'isrc123' ) assert batch_dir == 'batch123' assert release_dir == 'batch123/isrc123' assert resources_dir == 'batch123/isrc123/resources' @patch( 'src.app.config.TIKTOK_S3_BASE_FOLDERNAME', new_callable=PropertyMock(return_value='temp-fingerprinting') ) @patch( 'src.app.config.RESOURCES_DIRNAME', new_callable=PropertyMock(return_value='resources') ) def test_s3_remote_foldernames_base(mock_resources_dir, mock_base_dir): """Test override base remote dirnames.""" (batch_dir, release_dir, resources_dir) = index._s3_remote_foldernames( 'batch123', 'isrc123' ) assert batch_dir == 'batch123' assert release_dir == 'batch123/isrc123' assert resources_dir == 'batch123/isrc123/resources' @patch( 'src.app.config.TIKTOK_S3_BASE_FOLDERNAME', new_callable=PropertyMock(return_value='temp-fingerprinting/') ) @patch( 'src.app.config.RESOURCES_DIRNAME', new_callable=PropertyMock(return_value='resources/') ) def test_s3_remote_foldernames_rstrip(mock_resources_dir, mock_base_dir): """Test rstrip remote dirnames.""" (batch_dir, release_dir, resources_dir) = index._s3_remote_foldernames( 'batch123', 'isrc123' ) assert batch_dir == 'batch123' assert release_dir == 'batch123/isrc123' assert resources_dir == 'batch123/isrc123/resources' @patch('src.app.logger.exception') @patch('src.app.s3_sound_recordings.get_latest_sound_recording_version') @patch('src.app.generate_ddex') @patch('sentry_sdk.init') def test_handler_exception( mock_sentry, mock_generate_ddex, mock_s3_get, mock_logger_exception): """Test handler function exception.""" mock_s3_get.return_value = ( 'dummy OSR ID', """ { "track_connection": { "tracks": [] }, "assets": [], "isrc": "isrc-123" } """, False ) input_event = { 'sound_recording': { 'id': 'sr-123' }, 'upload_asset': False, 'execution_name': '6ff851fa-d83b-4e6e-9888-66077de9529a' } test_exception = Exception('test') mock_generate_ddex.side_effect = test_exception with pytest.raises(Exception): index.handler(input_event, None) mock_logger_exception.assert_called_once_with('test') assert not mock_sentry.called @patch('src.app.logger.exception') @patch('src.app.s3_sound_recordings.get_latest_sound_recording_version') @patch('sentry_sdk.init') def test_handler_exception_silent( mock_sentry, mock_s3_get, mock_logger_exception): """Test handler function exception not sent to setnry.""" mock_s3_get.return_value = ( 'dummy OSR ID', """ { "track_connection": { "tracks": [] }, "assets": [], "isrc": "isrc-123" } """, False ) input_event = { 'sound_recording': { 'id': 'sr-123' }, 'upload_asset': True, 'execution_name': '6ff851fa-d83b-4e6e-9888-66077de9529a' } with pytest.raises(index.NoAssets): index.handler(input_event, None) assert not mock_logger_exception.called assert mock_sentry.called @patch('src.app.random') @freeze_time('2019-06-15 12:45:17') def test_batch_id(mock_random): """Test batch id generate based on time and random.""" mock_random.randint.return_value = 10 result = index._generate_batch_id() assert result == '201906151245170010' assert mock_random.randint.call_args_list == [ call(0, 9999) ] @patch('config.secrets_manager_client') @patch('sentry_sdk.init') def test_handler_init(mock_sentry, mock_secrets): """Test sentry init with dsn.""" mock_secret_values = { 'SENTRY_DSN': 'fake-dsn', } mock_secrets.get_cred.side_effect = lambda x: mock_secret_values[x] assert not mock_sentry.called importlib.reload(index) assert mock_sentry.called assert mock_secrets.get_cred.call_args_list == [ mock.call('SENTRY_DSN'), ] @patch('config.secrets_manager_client') @patch('sentry_sdk.init') def test_handler_init_empty_sentry(mock_sentry, mock_secrets): """Test sentry init with dsn.""" mock_secret_values = { 'SENTRY_DSN': '', } mock_secrets.get_cred.side_effect = lambda x: mock_secret_values[x] assert not mock_sentry.called importlib.reload(index) assert not mock_sentry.called @patch('config.secrets_manager_client') @patch('sentry_sdk.init') def test_handler_init_none_sentry(mock_sentry, mock_secrets): """Test sentry init with dsn.""" mock_secret_values = { 'SENTRY_DSN': None, } mock_secrets.get_cred.side_effect = lambda x: mock_secret_values[x] assert not mock_sentry.called importlib.reload(index) assert not mock_sentry.called _MINIMAL_OSR_JSON = '{"track_connection": {"tracks": []}, "assets": [], "isrc": "isrc-123"}' _MINIMAL_EVENT = {'sound_recording': {'id': 'sr-123'}, 'upload_asset': False, 'execution_name': 'exec-1'} @patch('src.app.config.PERFORM_FINAL_DDEX_COMPARISON', True) @patch('src.app.s3_delivery_audit.download_sr_delivery_audit_xml') @patch('src.app.graphql.get_last_delivered_xml_location') @patch('src.app.graphql.get_delivery_histories') @patch('src.app._generate_batch_id') @patch('src.app.generate_ddex') @patch('src.app.s3_sound_recordings.get_latest_sound_recording_version') def test_handler_duplicate_ddex_returns_undelivered( mock_s3_get, mock_generate_ddex, mock_batch_id, mock_get_histories, mock_get_xml_location, mock_download_xml): """Test that an identical previous DDEX triggers an early undelivered response.""" mock_batch_id.return_value = 'batch123' mock_s3_get.return_value = ('dummy', _MINIMAL_OSR_JSON, False) mock_generate_ddex.return_value = 'XML Result' mock_get_histories.return_value = {'sr-123': {'status': 'SUCCESS'}} mock_get_xml_location.return_value = 'sfn/isrc-123/isrc-123.xml' mock_download_xml.return_value = b'XML Result' result = index.handler(_MINIMAL_EVENT, None) assert result == {'details': {'undelivered': True, 'reason': 'duplicate'}} mock_get_histories.assert_called_once_with(['sr-123'], 'TikTok (Audio Fingerprinting)') mock_get_xml_location.assert_called_once_with('sr-123') mock_download_xml.assert_called_once_with('sfn/isrc-123/isrc-123.xml') @patch('src.app.config.PERFORM_FINAL_DDEX_COMPARISON', True) @patch('src.app.s3_delivery_audit.write_sr_delivery_audit_xml') @patch('src.app.s3_asset_delivery.write_file') @patch('src.app.s3_delivery_audit.download_sr_delivery_audit_xml') @patch('src.app.graphql.get_last_delivered_xml_location') @patch('src.app.graphql.get_delivery_histories') @patch('src.app._s3_remote_foldernames') @patch('src.app._generate_batch_id') @patch('src.app.generate_ddex') @patch('src.app.s3_sound_recordings.get_latest_sound_recording_version') def test_handler_sr_not_in_history_skips_xml_check( mock_s3_get, mock_generate_ddex, mock_batch_id, mock_s3_foldernames, mock_get_histories, mock_get_xml_location, mock_download_xml, mock_write_to_s3, mock_write_to_s3_audit): """Test that get_last_delivered_xml_location is not called when SR has no delivery history.""" mock_batch_id.return_value = 'batch123' mock_s3_foldernames.return_value = ('batch123', 'batch123/isrc-123', 'batch123/isrc-123/resources') mock_s3_get.return_value = ('dummy', _MINIMAL_OSR_JSON, False) mock_generate_ddex.return_value = 'XML Result' mock_get_histories.return_value = {} result = index.handler(_MINIMAL_EVENT, None) assert 'batch_id' in result['details'] mock_get_xml_location.assert_not_called() mock_download_xml.assert_not_called() @patch('src.app.config.PERFORM_FINAL_DDEX_COMPARISON', True) @patch('src.app.s3_delivery_audit.write_sr_delivery_audit_xml') @patch('src.app.s3_asset_delivery.write_file') @patch('src.app.s3_delivery_audit.download_sr_delivery_audit_xml') @patch('src.app.graphql.get_last_delivered_xml_location') @patch('src.app.graphql.get_delivery_histories') @patch('src.app._s3_remote_foldernames') @patch('src.app._generate_batch_id') @patch('src.app.generate_ddex') @patch('src.app.s3_sound_recordings.get_latest_sound_recording_version') def test_handler_non_success_status_skips_xml_check( mock_s3_get, mock_generate_ddex, mock_batch_id, mock_s3_foldernames, mock_get_histories, mock_get_xml_location, mock_download_xml, mock_write_to_s3, mock_write_to_s3_audit): """Test that get_last_delivered_xml_location is not called when the last delivery was not successful.""" mock_batch_id.return_value = 'batch123' mock_s3_foldernames.return_value = ('batch123', 'batch123/isrc-123', 'batch123/isrc-123/resources') mock_s3_get.return_value = ('dummy', _MINIMAL_OSR_JSON, False) mock_generate_ddex.return_value = 'XML Result' mock_get_histories.return_value = {'sr-123': {'status': 'FAILED'}} result = index.handler(_MINIMAL_EVENT, None) assert 'batch_id' in result['details'] mock_get_xml_location.assert_not_called() mock_download_xml.assert_not_called() @patch('src.app.config.PERFORM_FINAL_DDEX_COMPARISON', True) @patch('src.app.s3_delivery_audit.write_sr_delivery_audit_xml') @patch('src.app.s3_asset_delivery.write_file') @patch('src.app.s3_delivery_audit.download_sr_delivery_audit_xml') @patch('src.app.graphql.get_last_delivered_xml_location') @patch('src.app.graphql.get_delivery_histories') @patch('src.app._s3_remote_foldernames') @patch('src.app._generate_batch_id') @patch('src.app.generate_ddex') @patch('src.app.s3_sound_recordings.get_latest_sound_recording_version') def test_handler_different_ddex_proceeds_with_delivery( mock_s3_get, mock_generate_ddex, mock_batch_id, mock_s3_foldernames, mock_get_histories, mock_get_xml_location, mock_download_xml, mock_write_to_s3, mock_write_to_s3_audit): """Test that a non-matching previous DDEX does not block delivery.""" mock_batch_id.return_value = 'batch123' mock_s3_foldernames.return_value = ('batch123', 'batch123/isrc-123', 'batch123/isrc-123/resources') mock_s3_get.return_value = ('dummy', _MINIMAL_OSR_JSON, False) mock_generate_ddex.return_value = 'XML Result' mock_get_histories.return_value = {'sr-123': {'status': 'SUCCESS'}} mock_get_xml_location.return_value = 'sfn/isrc-123/isrc-123.xml' mock_download_xml.return_value = b'Different XML' result = index.handler(_MINIMAL_EVENT, None) assert result == { 'details': { 'batch_id': 'batch123', 'filenames': ['batch123/isrc-123/isrc-123.xml', 'batch123/BatchComplete_batch123.xml'] } }