"""Tests for processing module.""" from unittest import mock import pytest from transcoding.constants import exceptions as transcoding_exceptions from transcoding.logic import job_status from transcoding.logic import metadata_capture from transcoding.logic import processing from transcoding.logic import storage from transcoding.logic import transcoding_logic from transcoding.utils import message_extractor from transcoding.validation import validate_job @pytest.fixture def transcoding_job_message_body(): """Return transcoding job message body.""" return { 'transcoding_job_id': 20, 'transcoding_order_id': 10, 'input_bucket': 'some_in_bucket', 'input_key': 'some_unique_in_filename.key', 'output_bucket': 'some_bucket', 'output_key': 'some_unique_filename.key'} @pytest.fixture def pass_through_message_body(): """Return pass-through job message body.""" return { 'transcoding_job_id': 20, 'transcoding_order_id': 10, 'input_bucket': 'some_in_bucket', 'input_key': 'some_unique_in_filename.key', 'output_bucket': 'some_bucket', 'output_key': 'some_unique_filename.key', 'pass_thru': True, } @pytest.fixture def asset_path(): """Return transcoded asset path.""" return '/some/file/path.ext' @pytest.fixture def asset_metadata(): """Return a mock metadata payload.""" return { 'duration_ms': 12345, 'channels': 2, 'sample_rate': 44100, 'bits_per_sample': 24, } @pytest.fixture def process_transcoding_job_mocks( transcoding_job_message_body, asset_metadata, asset_path): """Return subcall mocks for process_transcoding_job.""" return { 'get_body': mock.MagicMock(return_value=transcoding_job_message_body), 'validate_schema': mock.MagicMock(return_value=True), 'transcode_asset': mock.MagicMock(return_value=asset_path), 'upload_job_results': mock.MagicMock(return_value=True), 'post_completed_status': mock.MagicMock(return_value=True), 'get_metadata': mock.MagicMock(return_value=asset_metadata) } def test_process_transcoding_job_success( monkeypatch, transcoding_job_message_body, process_transcoding_job_mocks, asset_metadata, asset_path): """Test for success of process_transcoding_job.""" job_message = mock.MagicMock() tmp_path = '/some/tmp/path/' monkeypatch.setattr( message_extractor, 'get_message_body', value=process_transcoding_job_mocks['get_body']) monkeypatch.setattr( validate_job, 'validate_transcoding_job_schema', value=process_transcoding_job_mocks['validate_schema']) monkeypatch.setattr( transcoding_logic, 'transcode_asset', value=process_transcoding_job_mocks['transcode_asset']) monkeypatch.setattr( storage, 'upload_job_results', value=process_transcoding_job_mocks['upload_job_results']) monkeypatch.setattr( job_status, 'post_completed_status', value=process_transcoding_job_mocks['post_completed_status']) monkeypatch.setattr( metadata_capture, 'get_metadata', value=process_transcoding_job_mocks['get_metadata']) result, details = processing.process_transcoding_job( job_message, tmp_path) process_transcoding_job_mocks['get_body'].assert_called_with( job_message) process_transcoding_job_mocks['validate_schema'].assert_called_with( transcoding_job_message_body, False) process_transcoding_job_mocks['transcode_asset'].assert_called_with( job_message, tmp_path) process_transcoding_job_mocks['upload_job_results'].assert_called_with( asset_path, transcoding_job_message_body) process_transcoding_job_mocks['post_completed_status'].assert_called_with( transcoding_job_message_body['transcoding_job_id'], asset_metadata, ) metadata_capture.get_metadata.assert_called_with(asset_path) assert result is True @pytest.mark.parametrize('failing_mock', [ 'validate_schema', 'transcode_asset', 'upload_job_results']) def test_process_transcoding_job_subcall_fatal_error( monkeypatch, process_transcoding_job_mocks, failing_mock, transcoding_job_message_body): """Test for fatal error of process_transcoding_job subcall.""" job_message = mock.MagicMock() tmp_path = '/some/tmp/path/' monkeypatch.setattr( message_extractor, 'get_message_body', value=process_transcoding_job_mocks['get_body']) error_message = 'error_%s' % failing_mock exception = transcoding_exceptions.TranscodingFatalError(error_message) process_transcoding_job_mocks[failing_mock] = mock.MagicMock( side_effect=exception) monkeypatch.setattr( validate_job, 'validate_transcoding_job_schema', value=process_transcoding_job_mocks['validate_schema']) monkeypatch.setattr( transcoding_logic, 'transcode_asset', value=process_transcoding_job_mocks['transcode_asset']) monkeypatch.setattr( storage, 'upload_job_results', value=process_transcoding_job_mocks['upload_job_results']) error_message_mock = mock.MagicMock() monkeypatch.setattr( job_status, 'post_error_status', error_message_mock) result, details = processing.process_transcoding_job( job_message, tmp_path) expected_error = 'Transcoding fatal error: error_%s' % failing_mock error_message_mock.assert_called_with( transcoding_job_message_body['transcoding_job_id'], expected_error) assert result is False @pytest.mark.parametrize('failing_mock', [ 'validate_schema', 'transcode_asset', 'upload_job_results']) def test_process_transcoding_job_subcall_retryable_error( monkeypatch, process_transcoding_job_mocks, failing_mock, transcoding_job_message_body): """Test for retryable error of process_transcoding_job subcall.""" job_message = mock.MagicMock() tmp_path = '/some/tmp/path/' monkeypatch.setattr( message_extractor, 'get_message_body', value=process_transcoding_job_mocks['get_body']) error_message = 'error_%s' % failing_mock exception = transcoding_exceptions.TranscodingRetryableError(error_message) process_transcoding_job_mocks[failing_mock] = mock.MagicMock( side_effect=exception) monkeypatch.setattr( validate_job, 'validate_transcoding_job_schema', value=process_transcoding_job_mocks['validate_schema']) monkeypatch.setattr( transcoding_logic, 'transcode_asset', value=process_transcoding_job_mocks['transcode_asset']) monkeypatch.setattr( storage, 'upload_job_results', value=process_transcoding_job_mocks['upload_job_results']) error_message_mock = mock.MagicMock() monkeypatch.setattr( job_status, 'post_error_status', error_message_mock) result, details = processing.process_transcoding_job( job_message, tmp_path) expected_error = 'Transcoding retryable error: error_%s' % failing_mock error_message_mock.assert_called_with( transcoding_job_message_body['transcoding_job_id'], expected_error, retryable=True) assert result is False @pytest.mark.parametrize('failing_mock', [ 'validate_schema', 'transcode_asset', 'upload_job_results']) def test_process_transcoding_job_subcall_unexpected_exception( monkeypatch, process_transcoding_job_mocks, failing_mock, transcoding_job_message_body): """Test for unexpected exception of process_transcoding_job subcall.""" job_message = mock.MagicMock() tmp_path = '/some/tmp/path/' monkeypatch.setattr( message_extractor, 'get_message_body', value=process_transcoding_job_mocks['get_body']) error_message = 'error_%s' % failing_mock exception = Exception(error_message) process_transcoding_job_mocks[failing_mock] = mock.MagicMock( side_effect=exception) monkeypatch.setattr( validate_job, 'validate_transcoding_job_schema', value=process_transcoding_job_mocks['validate_schema']) monkeypatch.setattr( transcoding_logic, 'transcode_asset', value=process_transcoding_job_mocks['transcode_asset']) monkeypatch.setattr( storage, 'upload_job_results', value=process_transcoding_job_mocks['upload_job_results']) error_message_mock = mock.MagicMock() monkeypatch.setattr( job_status, 'post_error_status', error_message_mock) result, details = processing.process_transcoding_job( job_message, tmp_path) assert result is False def test_process_transcoding_job_success_metadata_failure( monkeypatch, transcoding_job_message_body, process_transcoding_job_mocks, asset_metadata, asset_path): """Test that a retryable error is raised when metadata capture fails.""" job_message = mock.MagicMock() tmp_path = '/some/tmp/path/' metadata_error = transcoding_exceptions.TranscodingRetryableError('whoops') get_metadata_mock = mock.MagicMock(side_effect=metadata_error) monkeypatch.setattr( message_extractor, 'get_message_body', value=process_transcoding_job_mocks['get_body']) monkeypatch.setattr( validate_job, 'validate_transcoding_job_schema', value=process_transcoding_job_mocks['validate_schema']) monkeypatch.setattr( transcoding_logic, 'transcode_asset', value=process_transcoding_job_mocks['transcode_asset']) monkeypatch.setattr( storage, 'upload_job_results', value=process_transcoding_job_mocks['upload_job_results']) monkeypatch.setattr( job_status, 'post_error_status', value=mock.MagicMock()) monkeypatch.setattr( metadata_capture, 'get_metadata', value=get_metadata_mock) result, details = processing.process_transcoding_job( job_message, tmp_path) job_status.post_error_status.assert_called_with( transcoding_job_message_body['transcoding_job_id'], str(metadata_error), retryable=True ) assert result is False assert details == str(metadata_error) def test_process_transcoding_job_pass_through_success( monkeypatch, pass_through_message_body, process_transcoding_job_mocks, asset_metadata): """Test for success of process_transcoding_job when pass_thru is True.""" job_message = mock.MagicMock() worker_id = 1 presigned_url = 'https://s3.example.com/presigned-url' get_body_mock = mock.MagicMock(return_value=pass_through_message_body) copy_object_mock = mock.MagicMock(return_value=None) generate_url_mock = mock.MagicMock(return_value=presigned_url) get_metadata_mock = mock.MagicMock(return_value=asset_metadata) transcode_asset_mock = mock.MagicMock() post_completed_status_mock = mock.MagicMock(return_value=True) monkeypatch.setattr( message_extractor, 'get_message_body', value=get_body_mock) monkeypatch.setattr( validate_job, 'validate_transcoding_job_schema', value=process_transcoding_job_mocks['validate_schema']) monkeypatch.setattr( storage, 'copy_object', value=copy_object_mock) monkeypatch.setattr( storage, 'generate_url_for_uploaded_asset', value=generate_url_mock) monkeypatch.setattr( metadata_capture, 'get_metadata', value=get_metadata_mock) monkeypatch.setattr( transcoding_logic, 'transcode_asset', value=transcode_asset_mock) monkeypatch.setattr( job_status, 'post_completed_status', value=post_completed_status_mock) result, details = processing.process_transcoding_job(job_message, worker_id) copy_object_mock.assert_called_once_with( pass_through_message_body['input_bucket'], pass_through_message_body['input_key'], pass_through_message_body['output_bucket'], pass_through_message_body['output_key'], ) generate_url_mock.assert_called_once_with( pass_through_message_body['output_bucket'], pass_through_message_body['output_key'], ) get_metadata_mock.assert_called_once_with(presigned_url) transcode_asset_mock.assert_not_called() post_completed_status_mock.assert_called_once_with( pass_through_message_body['transcoding_job_id'], asset_metadata, ) assert result is True assert details == '' @pytest.mark.parametrize('exception_cls, expected_error, extra_kwargs', [ ( transcoding_exceptions.TranscodingFatalError, 'Transcoding fatal error: copy error', {}, ), ( transcoding_exceptions.TranscodingRetryableError, 'Transcoding retryable error: copy error', {'retryable': True}, ), ]) def test_process_transcoding_job_pass_through_error( monkeypatch, pass_through_message_body, exception_cls, expected_error, extra_kwargs): """Test for error during pass_through processing.""" job_message = mock.MagicMock() worker_id = 1 monkeypatch.setattr( message_extractor, 'get_message_body', value=mock.MagicMock(return_value=pass_through_message_body)) monkeypatch.setattr( validate_job, 'validate_transcoding_job_schema', value=mock.MagicMock(return_value=True)) monkeypatch.setattr( storage, 'copy_object', value=mock.MagicMock(side_effect=exception_cls('copy error'))) post_error_status_mock = mock.MagicMock() monkeypatch.setattr(job_status, 'post_error_status', post_error_status_mock) result, details = processing.process_transcoding_job(job_message, worker_id) post_error_status_mock.assert_called_with( pass_through_message_body['transcoding_job_id'], expected_error, **extra_kwargs, ) assert result is False assert details == expected_error def test_process_transcoding_job_pass_through_unexpected_error( monkeypatch, pass_through_message_body): """Test for unexpected exception during pass_through processing.""" job_message = mock.MagicMock() worker_id = 1 monkeypatch.setattr( message_extractor, 'get_message_body', value=mock.MagicMock(return_value=pass_through_message_body)) monkeypatch.setattr( validate_job, 'validate_transcoding_job_schema', value=mock.MagicMock(return_value=True)) monkeypatch.setattr( storage, 'copy_object', value=mock.MagicMock(side_effect=Exception('unexpected'))) result, details = processing.process_transcoding_job(job_message, worker_id) assert result is False assert 'unexpected' in details @pytest.mark.parametrize('exception_cls, expected_error, extra_kwargs', [ ( transcoding_exceptions.TranscodingFatalError, 'Transcoding fatal error: url error', {}, ), ( transcoding_exceptions.TranscodingRetryableError, 'Transcoding retryable error: url error', {'retryable': True}, ), ]) def test_process_transcoding_job_pass_through_generate_url_error( monkeypatch, pass_through_message_body, exception_cls, expected_error, extra_kwargs): """Test for error during generate_url_for_uploaded_asset in pass-through.""" job_message = mock.MagicMock() worker_id = 1 monkeypatch.setattr( message_extractor, 'get_message_body', value=mock.MagicMock(return_value=pass_through_message_body)) monkeypatch.setattr( validate_job, 'validate_transcoding_job_schema', value=mock.MagicMock(return_value=True)) monkeypatch.setattr( storage, 'copy_object', value=mock.MagicMock(return_value=None)) monkeypatch.setattr( storage, 'generate_url_for_uploaded_asset', value=mock.MagicMock(side_effect=exception_cls('url error'))) post_error_status_mock = mock.MagicMock() monkeypatch.setattr(job_status, 'post_error_status', post_error_status_mock) result, details = processing.process_transcoding_job(job_message, worker_id) post_error_status_mock.assert_called_with( pass_through_message_body['transcoding_job_id'], expected_error, **extra_kwargs, ) assert result is False assert details == expected_error @pytest.mark.parametrize('exception_cls, expected_error, extra_kwargs', [ ( transcoding_exceptions.TranscodingFatalError, 'Transcoding fatal error: metadata error', {}, ), ( transcoding_exceptions.TranscodingRetryableError, 'Transcoding retryable error: metadata error', {'retryable': True}, ), ]) def test_process_transcoding_job_pass_through_metadata_error( monkeypatch, pass_through_message_body, exception_cls, expected_error, extra_kwargs): """Test for error during metadata capture in pass-through.""" job_message = mock.MagicMock() worker_id = 1 presigned_url = 'https://s3.example.com/presigned-url' monkeypatch.setattr( message_extractor, 'get_message_body', value=mock.MagicMock(return_value=pass_through_message_body)) monkeypatch.setattr( validate_job, 'validate_transcoding_job_schema', value=mock.MagicMock(return_value=True)) monkeypatch.setattr( storage, 'copy_object', value=mock.MagicMock(return_value=None)) monkeypatch.setattr( storage, 'generate_url_for_uploaded_asset', value=mock.MagicMock(return_value=presigned_url)) monkeypatch.setattr( metadata_capture, 'get_metadata', value=mock.MagicMock(side_effect=exception_cls('metadata error'))) post_error_status_mock = mock.MagicMock() monkeypatch.setattr(job_status, 'post_error_status', post_error_status_mock) result, details = processing.process_transcoding_job(job_message, worker_id) post_error_status_mock.assert_called_with( pass_through_message_body['transcoding_job_id'], expected_error, **extra_kwargs, ) assert result is False assert details == expected_error @pytest.mark.parametrize('invalid_body', [ 'some string', ['list', 'value'], 123, None, ]) def test_process_transcoding_job_non_dict_message_body( monkeypatch, invalid_body): """Test that a non-dict message body results in a TranscodingFatalError.""" job_message = mock.MagicMock() worker_id = 1 monkeypatch.setattr( message_extractor, 'get_message_body', mock.MagicMock(return_value=invalid_body)) post_error_status_mock = mock.MagicMock() monkeypatch.setattr(job_status, 'post_error_status', post_error_status_mock) result, details = processing.process_transcoding_job(job_message, worker_id) expected_error = 'Transcoding fatal error: Message body is not a valid JSON object.' post_error_status_mock.assert_called_with(None, expected_error) assert result is False assert details == expected_error