"""Tests for transcoding_job logic module.""" from typing import Any import pytest from flexmock import flexmock from owsresponse import response from transcoding.constants import error, transcoding as transcoding_constants from transcoding.logic import ( asset_status, messaging, transcoding_order as transcoding_order_logic, transcoding_status, ) from transcoding.models import transcoding_job as transcoding_job_model @pytest.fixture def transcoding_job_fixture() -> dict[str, Any]: """Return transcoding_job fixture.""" return { "transcoding_job_id": 20, "transcoding_order_id": 10, "status": "requested", "description": None, "attempt": 0, "output_bucket": "ows-transcoding-raw", "output_key": "4e6e4670-03f4-4a34-8d92-123123dc.wav", "container": "m4a", "channels": 4, "codec": "PCM", "sample_rate": 44100, "bit_rate": 1411200, "bit_depth": 16, "preset_id": None, } @pytest.fixture def process_success_transcoding_status_args( transcoding_job_fixture: dict[str, Any], ) -> dict[str, Any]: """Return call args for _process_success_status.""" return { "transcoding_job_data": transcoding_job_fixture, "output_bucket": "bucket", "output_key": "filename", "metadata": { "@type": "Audio", "Format": "PCM", "Format_Settings_Endianness": "Little", "Format_Settings_Sign": "Signed", "CodecID": "1", "Duration": "2.963", "BitRate_Mode": "CBR", "BitRate": "1411200", "Channels": "2", "SamplingRate": "44100", "SamplingCount": "130688", "BitDepth": "16", "StreamSize": "522752", }, } @pytest.fixture def process_failed_transcoding_status_args( transcoding_job_fixture: dict[str, Any], ) -> dict[str, Any]: """Return call args for _process_failed_status.""" return { "transcoding_job_data": transcoding_job_fixture, "status_description": "failure_details", } @pytest.fixture def process_retryable_error_transcoding_status_args( transcoding_job_fixture: dict[str, Any], ) -> dict[str, Any]: """Return call args for _process_retryable_error_status.""" return { "transcoding_job_data": transcoding_job_fixture, "status_description": "retryable_status_details", } @pytest.fixture def fixture_get_order_status() -> dict[str, Any]: """Return get_order_status fixture.""" return { "transcoding_order_id": 10, "status": "completed", "transcoding_jobs": [ { "status": "completed", "transcoding_job_id": 1, "description": "", "output_key": "output_key_1.ext", "output_bucket": "output_bucket_1", "container": "flac", "duration": 1000, "channels": 2, "codec": "flac", "sample_rate": 44100, "bit_rate": None, "bit_depth": 16, }, { "status": "completed", "transcoding_job_id": 2, "description": "", "output_key": "output_key_2.ext", "output_bucket": "output_bucket_2", "container": "flac", "duration": 2000, "channels": 2, "codec": "flac", "sample_rate": 44100, "bit_rate": None, "bit_depth": 16, }, ], } @pytest.fixture def fixture_result_assets() -> list[dict[str, Any]]: """Return final result assets fixture.""" return [ { "key": "output_key_1.ext", "bucket": "output_bucket_1", "container": "flac", "duration": 1000, "channels": 2, "codec": "flac", "sample_rate": 44100, "bit_rate": None, "bit_depth": 16, }, { "key": "output_key_2.ext", "bucket": "output_bucket_2", "container": "flac", "duration": 2000, "channels": 2, "codec": "flac", "sample_rate": 44100, "bit_rate": None, "bit_depth": 16, }, ] @pytest.fixture def process_completed_status_mocks( fixture_get_order_status: dict[str, Any], ) -> dict[str, Any]: """Return mocks for _process_completed_status call.""" return { "update_transcoding_job_status": response.Response(), "get_order_status": response.Response(fixture_get_order_status), "send_notification": response.Response(), } def test_process_completed_status_success( process_success_transcoding_status_args: dict[str, Any], transcoding_job_fixture: dict[str, Any], process_completed_status_mocks: dict[str, Any], fixture_result_assets: list[dict[str, Any]], ) -> None: """Test for _process_completed_status success.""" ( flexmock(transcoding_job_model) .should_receive("update_transcoding_job_status") .with_args( transcoding_job_id=transcoding_job_fixture["transcoding_job_id"], new_status=transcoding_constants.COMPLETED_STATUS, output_bucket=process_success_transcoding_status_args["output_bucket"], output_key=process_success_transcoding_status_args["output_key"], metadata=process_success_transcoding_status_args["metadata"], ) .and_return(process_completed_status_mocks["update_transcoding_job_status"]) .once() ) ( flexmock(transcoding_order_logic) .should_receive("get_status") .with_args(transcoding_order_id=transcoding_job_fixture["transcoding_order_id"]) .and_return(process_completed_status_mocks["get_order_status"]) .once() ) ( flexmock(asset_status) .should_receive("update_asset_final_status") .with_args( transcoding_job_id=transcoding_job_fixture["transcoding_job_id"], status=transcoding_constants.COMPLETED_STATUS, final_assets=fixture_result_assets, ) .and_return(process_completed_status_mocks["send_notification"]) .once() ) result = transcoding_status._process_completed_status( **process_success_transcoding_status_args ) assert result assert result.message == {"status": error.SUCCESS_CODE} @pytest.mark.parametrize( "failing_mock", ["update_transcoding_job_status", "get_order_status", "send_notification"], ) def test_process_completed_status_failure( process_success_transcoding_status_args: dict[str, Any], process_completed_status_mocks: dict[str, Any], failing_mock: str, ) -> None: """Test for _process_completed_status subcall failure.""" error_message = "fatal_error_%s" % failing_mock error_response = response.create_not_found_response(error_message) process_completed_status_mocks[failing_mock] = error_response ( flexmock(transcoding_job_model) .should_receive("update_transcoding_job_status") .and_return(process_completed_status_mocks["update_transcoding_job_status"]) ) ( flexmock(transcoding_order_logic) .should_receive("get_status") .and_return(process_completed_status_mocks["get_order_status"]) ) ( flexmock(asset_status) .should_receive("update_asset_final_status") .and_return(process_completed_status_mocks["send_notification"]) ) result = transcoding_status._process_completed_status( **process_success_transcoding_status_args ) assert not result assert result.errors["message"] == error_message def test_process_completed_status_success_no_result_assets( process_success_transcoding_status_args: dict[str, Any], transcoding_job_fixture: dict[str, Any], process_completed_status_mocks: dict[str, Any], ) -> None: """Test for process_transcoding_status completed or fatal_error.""" ( flexmock(transcoding_job_model) .should_receive("update_transcoding_job_status") .with_args( transcoding_job_id=transcoding_job_fixture["transcoding_job_id"], new_status=transcoding_constants.COMPLETED_STATUS, output_bucket=process_success_transcoding_status_args["output_bucket"], output_key=process_success_transcoding_status_args["output_key"], metadata=process_success_transcoding_status_args["metadata"], ) .and_return(process_completed_status_mocks["update_transcoding_job_status"]) .once() ) status_mock = process_completed_status_mocks["get_order_status"] status_mock_message = status_mock.message status_mock_message["status"] = "not_completed" ( flexmock(transcoding_order_logic) .should_receive("get_status") .with_args(transcoding_order_id=transcoding_job_fixture["transcoding_order_id"]) .and_return(status_mock) .once() ) ( flexmock(asset_status) .should_receive("update_asset_final_status") .and_return(process_completed_status_mocks["send_notification"]) .times(0) ) result = transcoding_status._process_completed_status( **process_success_transcoding_status_args ) assert result assert result.message == {"status": error.SUCCESS_CODE} @pytest.fixture def process_failed_status_mocks() -> dict[str, Any]: """Return mocks for _process_failed_status call.""" return { "update_transcoding_job_status": response.Response(), "send_notification": response.Response(), } def test_process_failed_status_success( process_failed_transcoding_status_args: dict[str, Any], process_failed_status_mocks: dict[str, Any], ) -> None: """Test for failure of _process_failed_status.""" ( flexmock(transcoding_job_model) .should_receive("update_transcoding_job_status") .with_args( transcoding_job_id=process_failed_transcoding_status_args[ "transcoding_job_data" ]["transcoding_job_id"], new_status=transcoding_constants.ERROR_STATUS, new_description=process_failed_transcoding_status_args[ "status_description" ], ) .and_return(process_failed_status_mocks["update_transcoding_job_status"]) ) ( flexmock(asset_status) .should_receive("update_asset_final_status") .with_args( transcoding_job_id=process_failed_transcoding_status_args[ "transcoding_job_data" ]["transcoding_job_id"], status=transcoding_constants.ERROR_STATUS, status_description=process_failed_transcoding_status_args[ "status_description" ], ) .and_return(process_failed_status_mocks["send_notification"]) ) result = transcoding_status._process_failed_status( **process_failed_transcoding_status_args ) assert result assert result.message == {"status": error.SUCCESS_CODE} @pytest.mark.parametrize( "failing_mock", ["update_transcoding_job_status", "send_notification"] ) def test_process_failed_status_failure( process_failed_transcoding_status_args: dict[str, Any], process_failed_status_mocks: dict[str, Any], failing_mock: str, ) -> None: """Test for failure of _process_failed_status.""" error_message = "error_%s" % failing_mock error_response = response.create_fatal_response(error_message) process_failed_status_mocks[failing_mock] = error_response ( flexmock(transcoding_job_model) .should_receive("update_transcoding_job_status") .and_return(process_failed_status_mocks["update_transcoding_job_status"]) ) ( flexmock(asset_status) .should_receive("update_asset_final_status") .and_return(process_failed_status_mocks["send_notification"]) ) result = transcoding_status._process_failed_status( **process_failed_transcoding_status_args ) assert not result assert result.errors["message"] == error_message @pytest.fixture def retryable_error_mocks(transcoding_job_fixture: dict[str, Any]) -> dict[str, Any]: """Return mocks for _process_retryable_error_status call.""" return { "update_attempt_counter": response.Response(), "send_notification": response.Response(), "update_transcoding_job_status": response.Response(), "resend_transcoding_job": response.Response(), } def test_process_retryable_error_status_attempts_no_overflow_success( process_retryable_error_transcoding_status_args: dict[str, Any], retryable_error_mocks: dict[str, Any], transcoding_job_fixture: dict[str, Any], ) -> None: """Test for process_transcoding_status retryable error success.""" ( flexmock(transcoding_job_model) .should_receive("update_transcoding_job_attempt_counter") .with_args( transcoding_job_id=transcoding_job_fixture["transcoding_job_id"], attempt=transcoding_job_fixture["attempt"] + 1, ) .and_return(retryable_error_mocks["update_attempt_counter"]) .once() ) ( flexmock(messaging) .should_receive("resend_transcoding_job_sqs_message") .with_args(transcoding_job_id=transcoding_job_fixture["transcoding_job_id"]) .and_return(retryable_error_mocks["resend_transcoding_job"]) .once() ) result = transcoding_status._process_retryable_error_status( **process_retryable_error_transcoding_status_args ) assert result assert result.message == {"status": error.SUCCESS_CODE} @pytest.mark.parametrize( "failing_mock", ["update_attempt_counter", "resend_transcoding_job"] ) def test_process_retryable_error_status_attempts_no_overflow_failure( process_retryable_error_transcoding_status_args: dict[str, Any], retryable_error_mocks: dict[str, Any], failing_mock: str, ) -> None: """Test for _process_retryable_error_status subcall failure.""" error_message = "failing_mock_%s" % failing_mock error_response = response.create_not_found_response(error_message) retryable_error_mocks[failing_mock] = error_response ( flexmock(transcoding_job_model) .should_receive("update_transcoding_job_attempt_counter") .and_return(retryable_error_mocks["update_attempt_counter"]) ) ( flexmock(messaging) .should_receive("resend_transcoding_job_sqs_message") .and_return(retryable_error_mocks["resend_transcoding_job"]) ) result = transcoding_status._process_retryable_error_status( **process_retryable_error_transcoding_status_args ) assert not result assert result.errors["message"] == error_message def test_process_retryable_error_status_attempts_overflow_success( process_retryable_error_transcoding_status_args: dict[str, Any], retryable_error_mocks: dict[str, Any], transcoding_job_fixture: dict[str, Any], ) -> None: """Test for process_transcoding_status retryable error success.""" process_retryable_error_transcoding_status_args["transcoding_job_data"][ "attempt" ] = transcoding_constants.MAX_ATTEMPT_COUNT new_description = "All attempts failed. Last error: {description}".format( description=process_retryable_error_transcoding_status_args[ "status_description" ] ) ( flexmock(transcoding_job_model) .should_receive("update_transcoding_job_status") .with_args( transcoding_job_id=transcoding_job_fixture["transcoding_job_id"], new_status=transcoding_constants.ERROR_STATUS, new_description=new_description, ) .and_return(retryable_error_mocks["update_transcoding_job_status"]) .once() ) ( flexmock(asset_status) .should_receive("update_asset_final_status") .with_args( transcoding_job_id=transcoding_job_fixture["transcoding_job_id"], status=transcoding_constants.ERROR_STATUS, status_description=new_description, ) .and_return(retryable_error_mocks["send_notification"]) .once() ) result = transcoding_status._process_retryable_error_status( **process_retryable_error_transcoding_status_args ) assert result assert result.message == {"status": error.SUCCESS_CODE} @pytest.mark.parametrize( "failing_mock", ["update_transcoding_job_status", "send_notification"] ) def test_process_retryable_error_status_attempts_overflow_failure( process_retryable_error_transcoding_status_args: dict[str, Any], retryable_error_mocks: dict[str, Any], failing_mock: str, ) -> None: """Test for process_transcoding_status retryable error success.""" process_retryable_error_transcoding_status_args["transcoding_job_data"][ "attempt" ] = transcoding_constants.MAX_ATTEMPT_COUNT error_message = "failing_mock_%s" % failing_mock error_response = response.create_not_found_response(error_message) retryable_error_mocks[failing_mock] = error_response ( flexmock(transcoding_job_model) .should_receive("update_transcoding_job_status") .and_return(retryable_error_mocks["update_transcoding_job_status"]) ) ( flexmock(asset_status) .should_receive("update_asset_final_status") .and_return(retryable_error_mocks["send_notification"]) ) result = transcoding_status._process_retryable_error_status( **process_retryable_error_transcoding_status_args ) assert not result assert result.errors["message"] == error_message @pytest.fixture def process_transcoding_status_args() -> dict[str, Any]: """Return call args for process_transcoding_status.""" return { "transcoding_job_id": 20, "status": "some_status", "status_description": "some_description", } @pytest.fixture def process_transcoding_status_mocks() -> dict[str, Any]: """Return call args for process_transcoding_status.""" return { "completed": response.Response("completed_response"), "failed": response.Response("failed_response"), "retryable": response.Response("retryable_response"), } @pytest.mark.parametrize( "status,mock", [ (transcoding_constants.JOB_STATUS_COMPLETED, "completed"), (transcoding_constants.JOB_STATUS_FATAL_ERROR, "failed"), (transcoding_constants.JOB_STATUS_RETRYABLE_ERROR, "retryable"), ], ) def test_process_transcoding_status_success( process_transcoding_status_args: dict[str, Any], status: str, mock: str, process_transcoding_status_mocks: dict[str, Any], transcoding_job_fixture: dict[str, Any], ) -> None: """Test for correct choice of status handler.""" process_transcoding_status_args["status"] = status ( flexmock(transcoding_job_model) .should_receive("get_transcoding_job_by_id") .with_args(process_transcoding_status_args["transcoding_job_id"]) .and_return(response.Response(transcoding_job_fixture)) ) ( flexmock(transcoding_status) .should_receive("_process_completed_status") .and_return(process_transcoding_status_mocks["completed"]) ) ( flexmock(transcoding_status) .should_receive("_process_failed_status") .and_return(process_transcoding_status_mocks["failed"]) ) ( flexmock(transcoding_status) .should_receive("_process_retryable_error_status") .and_return(process_transcoding_status_mocks["retryable"]) ) result = transcoding_status.process_transcoding_status( **process_transcoding_status_args ) assert result assert result.message == process_transcoding_status_mocks[mock].message def test_process_transcoding_status_not_found( process_transcoding_status_args: dict[str, Any], ) -> None: """Test for correct choice of status handler.""" ( flexmock(transcoding_job_model) .should_receive("get_transcoding_job_by_id") .with_args(process_transcoding_status_args["transcoding_job_id"]) .and_return(response.create_not_found_response("message")) ) result = transcoding_status.process_transcoding_status( **process_transcoding_status_args ) assert not result assert result.status == 404 def test_process_transcoding_status_unknown_status( process_transcoding_status_args: dict[str, Any], process_transcoding_status_mocks: dict[str, Any], transcoding_job_fixture: dict[str, Any], ) -> None: """Test for unknown status.""" process_transcoding_status_args["status"] = "unknown" ( flexmock(transcoding_job_model) .should_receive("get_transcoding_job_by_id") .with_args(process_transcoding_status_args["transcoding_job_id"]) .and_return(response.Response(transcoding_job_fixture)) ) ( flexmock(transcoding_status) .should_receive("_process_completed_status") .and_return(process_transcoding_status_mocks["completed"]) ) ( flexmock(transcoding_status) .should_receive("_process_failed_status") .and_return(process_transcoding_status_mocks["failed"]) ) ( flexmock(transcoding_status) .should_receive("_process_retryable_error_status") .and_return(process_transcoding_status_mocks["retryable"]) ) result = transcoding_status.process_transcoding_status( **process_transcoding_status_args ) assert not result assert result.status == 400