"""Tests for the jobs logic layer.""" import hashlib from datetime import UTC, datetime, timedelta from unittest.mock import Mock, _Call, call import pytest from vectororder.constants import ( fields, jobs as jobs_const, ) from vectororder.constants.error import ( ERROR_CODE_INCORRECT_STATUS, ERROR_CODE_JOBS_NOT_EXIST, ERROR_CODE_OLDER_DUPLICATE, ERROR_MESSAGE_INCORRECT_STATUS, ERROR_MESSAGE_JOBS_NOT_EXIST, ERROR_MESSAGE_OLDER_DUPLICATE, ) from vectororder.exceptions import ( DeliveryFileNotAvailableError, DeliveryFileNotFoundError, ProductNotFoundError, ProductNotInContent, VendorNotFoundError, ) from vectororder.logic import jobs as job_logic from vectororder.logic.jobs import _get_carveouts_distro, get_job_delivery_xml_payload from vectororder.logic.schemas import JobEligibility, JobIneligibilityReason from vectororder.models import s3_file from vectororder.models.schemas import ( DeliveryType, DistributionFeatureId, DmsDeliverySpec, DownloadStreamRights, EncodingOrderType, Job, JobStatus, Product, Store, Track, TrackOfferType, TrackType, Vendor, ) def test_get_error() -> None: """Test _get_error function.""" code = "err code" message = "err message" ids = [1, 2] result = job_logic._get_error(code, message, ids) assert result == { fields.RESPONSE_ERROR_CODE: code, fields.RESPONSE_ERROR_MESSAGE: message, fields.RESPONSE_ERROR_JOB_IDS: ids, } @pytest.mark.parametrize( "error, job_statuses, expected_statuses", ( ("err1", None, None), ({fields.RESPONSE_ERROR_JOB_IDS: [10]}, {10: "a", 11: "b"}, {11: "b"}), ), ) def test_process_check_results( error: dict[str, str | list[int]], job_statuses: dict[int, JobStatus] | None, expected_statuses: dict[int, JobStatus], ) -> None: """Test _process_check_results function.""" errors: list[dict[str, str | list[int]]] = [] expected_errors = [error] job_logic._process_check_results(error, errors, job_statuses) assert errors == expected_errors assert job_statuses == expected_statuses def test_process_check_results_seq() -> None: """Test _process_check_results, multiple calls.""" errors: list[dict[str, str | list[int]]] = [] err1: dict[str, str | list[int]] = {"e": "err1"} err2: dict[str, str | list[int]] = {"e": "err2"} job_logic._process_check_results(err1, errors) job_logic._process_check_results(err2, errors) assert errors == [err1, err2] def get_ids(offset: int = 0, limit: int = 3) -> list[int]: """Get fake IDs list. Args: offset (int): ID interval range from limit (int): ID interval range to Returns: list: fake IDs """ return list(range(offset, limit)) def get_statuses( statuses: list[JobStatus] | None = None, processed: bool = True, date_diff: int = 1, ) -> dict[int, JobStatus] | tuple[list[int], dict[int, Job]]: """Get fake statuses data for testing. Can provide fake job IDs, model statuses result, logic processed statuses result. Args: statuses (tuple): statuses source for fake records processed (bool): model or logic statuses date_diff (int): interval for start date from now (hours) Returns: dict or tuple: logic statuses dict (key - job id (int), value - status (str)) or fake ID list and model statuses dict in tuple """ if not statuses: statuses = [ JobStatus.ENCODING, JobStatus.DELIVERING, JobStatus.SYSTEM_CANCELLED, ] job_ids = get_ids(limit=len(statuses)) if processed: job_statuses = {} for job_id, status in zip(job_ids, statuses, strict=False): job_statuses[job_id] = status return job_statuses start_date = datetime.now(UTC) - timedelta(hours=date_diff) return job_ids, { job_id: Job( job_id=job_id, upc=12341234123, store_id=1, delivery_type=DeliveryType.METADATA_UPDATE, encoding_order_type=EncodingOrderType.RELEASE, status=status, encoding_started=start_date, delivery_started=start_date, ) for job_id, status in zip(job_ids, statuses, strict=False) } @pytest.mark.parametrize( "ids_statuses, expected_result", [ ((get_ids(), {}), {}), (get_statuses(processed=False), get_statuses()), ( get_statuses(processed=False, date_diff=34), get_statuses( statuses=[ JobStatus.ENCODING_STUCK, JobStatus.DELIVERING_STUCK, JobStatus.SYSTEM_CANCELLED, ] ), ), ], ) def test_get_statuses( ids_statuses: tuple[list[int], dict[int, Job]], expected_result: dict[int, JobStatus], mocker: Mock, ) -> None: """Test _get_statuses function.""" job_ids, statuses_result = ids_statuses mocked_jobs = mocker.patch("vectororder.logic.jobs.job_model.get_jobs") mocked_jobs.return_value = statuses_result mocked_config = mocker.patch("vectororder.logic.jobs.config") mocked_config.STATUS_STUCK_MINUTES = 60 * 24 result = job_logic._get_statuses(job_ids) assert mocked_jobs.called assert mocked_jobs.call_args[0] == (job_ids,) assert result == expected_result @pytest.mark.parametrize( "job_ids, job_statuses, expected_result", ( (get_ids(), get_statuses(), []), (get_ids(limit=1), get_statuses(), []), ([1, 1, 1, 2], get_statuses(), []), (get_ids(limit=5), get_statuses(), get_ids(offset=3, limit=5)), ), ) def test_check_exist( job_ids: list[int], job_statuses: dict[int, JobStatus], expected_result: list[int], ) -> None: """Test _check_exist function.""" result = job_logic._check_exist(job_ids, job_statuses) assert result == expected_result @pytest.mark.parametrize( "job_statuses, jobs_dupes, call_count, expected_result", [ (get_statuses(), [], 1, []), (get_statuses(), get_ids(limit=2), 1, get_ids(limit=2)), ({}, None, 0, []), ], ) def test_check_duplicates( job_statuses: dict[int, JobStatus], jobs_dupes: list[int], call_count: int, expected_result: list[int], mocker: Mock, ) -> None: """Test _check_duplicates function.""" job_ids = list(job_statuses.keys()) mocked_jobs_model = mocker.patch( "vectororder.logic.jobs.job_model.get_jobs_duplicates" ) mocked_jobs_model.return_value = jobs_dupes result = job_logic._check_duplicates(job_statuses) assert mocked_jobs_model.call_count == call_count if call_count: assert mocked_jobs_model.call_args[0] == (job_ids,) assert expected_result == result @pytest.mark.parametrize( "action, job_statuses, expected_result", ( ( jobs_const.JOB_ACTION_REENCODE, get_statuses( statuses=jobs_const.ALLOWED_STATUSES[jobs_const.JOB_ACTION_REENCODE] ), [], ), ( jobs_const.JOB_ACTION_REDELIVER, get_statuses( statuses=jobs_const.ALLOWED_STATUSES[jobs_const.JOB_ACTION_REDELIVER] ), [], ), ( jobs_const.JOB_ACTION_CANCEL, get_statuses( statuses=jobs_const.ALLOWED_STATUSES[jobs_const.JOB_ACTION_CANCEL] ), [], ), ( jobs_const.JOB_ACTION_REENCODE, get_statuses(statuses=[JobStatus.ENCODING, JobStatus.ENCODING_STUCK]), get_ids(limit=1), ), ( jobs_const.JOB_ACTION_REDELIVER, get_statuses(statuses=[JobStatus.DELIVERING, JobStatus.DELIVERING_STUCK]), get_ids(limit=1), ), ( jobs_const.JOB_ACTION_CANCEL, get_statuses(statuses=[JobStatus.ENCODING, JobStatus.ENCODING_STUCK]), get_ids(limit=1), ), ), ) def test_check_statuses( action: str, job_statuses: dict[int, JobStatus], expected_result: list[int] ) -> None: """Test _check_statuses function.""" result = job_logic._check_statuses(action, job_statuses) assert result == expected_result @pytest.mark.parametrize( ( "action", "job_ids", "get_statuses_result", "get_job_duplicates_result", "expected_result", "expected_errors", ), [ (jobs_const.JOB_ACTION_REENCODE, [], {}, [], {}, []), ( jobs_const.JOB_ACTION_REENCODE, get_ids(), get_statuses(processed=False)[1], [], {}, [ { fields.RESPONSE_ERROR_CODE: ERROR_CODE_INCORRECT_STATUS, fields.RESPONSE_ERROR_MESSAGE: ERROR_MESSAGE_INCORRECT_STATUS, fields.RESPONSE_ERROR_JOB_IDS: get_ids(), }, ], ), ( jobs_const.JOB_ACTION_REENCODE, get_ids(), get_statuses( statuses=[JobStatus.ENCODING_STUCK, JobStatus.ENCODING], processed=False )[1], [], {0: JobStatus.ENCODING_STUCK}, [ { fields.RESPONSE_ERROR_CODE: ERROR_CODE_JOBS_NOT_EXIST, fields.RESPONSE_ERROR_MESSAGE: ERROR_MESSAGE_JOBS_NOT_EXIST, fields.RESPONSE_ERROR_JOB_IDS: get_ids(limit=3, offset=2), }, { fields.RESPONSE_ERROR_CODE: ERROR_CODE_INCORRECT_STATUS, fields.RESPONSE_ERROR_MESSAGE: ERROR_MESSAGE_INCORRECT_STATUS, fields.RESPONSE_ERROR_JOB_IDS: get_ids(limit=2, offset=1), }, ], ), ( jobs_const.JOB_ACTION_REENCODE, get_ids(), get_statuses( statuses=[ JobStatus.AUDIO_MISSING, JobStatus.SYSTEM_ERROR, JobStatus.DELIVERY_FAILURE, ], processed=False, )[1], [], { 0: JobStatus.AUDIO_MISSING, 1: JobStatus.SYSTEM_ERROR, 2: JobStatus.DELIVERY_FAILURE, }, [], ), ( jobs_const.JOB_ACTION_REENCODE, get_ids(), get_statuses( statuses=[ JobStatus.METADATA_ERROR, JobStatus.INCORRECT_AUDIO_COUNT, JobStatus.VIDEO_MISSING, ], processed=False, )[1], get_ids(), {}, [ { fields.RESPONSE_ERROR_CODE: ERROR_CODE_OLDER_DUPLICATE, fields.RESPONSE_ERROR_MESSAGE: ERROR_MESSAGE_OLDER_DUPLICATE, fields.RESPONSE_ERROR_JOB_IDS: get_ids(), }, ], ), ( jobs_const.JOB_ACTION_REDELIVER, get_ids(), get_statuses(processed=False)[1], [], {}, [ { fields.RESPONSE_ERROR_CODE: ERROR_CODE_INCORRECT_STATUS, fields.RESPONSE_ERROR_MESSAGE: ERROR_MESSAGE_INCORRECT_STATUS, fields.RESPONSE_ERROR_JOB_IDS: get_ids(), }, ], ), ( jobs_const.JOB_ACTION_REDELIVER, get_ids(), get_statuses( statuses=[JobStatus.ENCODING_STUCK, JobStatus.ENCODING], processed=False )[1], [], {}, [ { fields.RESPONSE_ERROR_CODE: ERROR_CODE_JOBS_NOT_EXIST, fields.RESPONSE_ERROR_MESSAGE: ERROR_MESSAGE_JOBS_NOT_EXIST, fields.RESPONSE_ERROR_JOB_IDS: get_ids(limit=3, offset=2), }, { fields.RESPONSE_ERROR_CODE: ERROR_CODE_INCORRECT_STATUS, fields.RESPONSE_ERROR_MESSAGE: ERROR_MESSAGE_INCORRECT_STATUS, fields.RESPONSE_ERROR_JOB_IDS: get_ids(limit=2), }, ], ), ( jobs_const.JOB_ACTION_REDELIVER, get_ids(), get_statuses( statuses=[ JobStatus.ENCODING_STUCK, JobStatus.SYSTEM_ERROR, JobStatus.DELIVERY_FAILURE, ], processed=False, )[1], [], {2: JobStatus.DELIVERY_FAILURE}, [ { fields.RESPONSE_ERROR_CODE: ERROR_CODE_INCORRECT_STATUS, fields.RESPONSE_ERROR_MESSAGE: ERROR_MESSAGE_INCORRECT_STATUS, fields.RESPONSE_ERROR_JOB_IDS: get_ids(limit=2), }, ], ), ( jobs_const.JOB_ACTION_REDELIVER, get_ids(), get_statuses( statuses=[ JobStatus.METADATA_MISSING, JobStatus.AUDIO_MISSING_FOR_DELIVERY, JobStatus.IMAGE_MISSING_FOR_DELIVERY, ], processed=False, )[1], get_ids(), {}, [ { fields.RESPONSE_ERROR_CODE: ERROR_CODE_INCORRECT_STATUS, fields.RESPONSE_ERROR_MESSAGE: ERROR_MESSAGE_INCORRECT_STATUS, fields.RESPONSE_ERROR_JOB_IDS: get_ids(), }, ], ), ( jobs_const.JOB_ACTION_CANCEL, [], get_statuses(processed=False)[1], [], {2: JobStatus.SYSTEM_CANCELLED}, [ { fields.RESPONSE_ERROR_CODE: ERROR_CODE_INCORRECT_STATUS, fields.RESPONSE_ERROR_MESSAGE: ERROR_MESSAGE_INCORRECT_STATUS, fields.RESPONSE_ERROR_JOB_IDS: [0, 1], }, ], ), ( jobs_const.JOB_ACTION_CANCEL, get_ids(), get_statuses( statuses=[ JobStatus.IMAGE_MISSING, JobStatus.AUDIO_MISSING_FOR_DELIVERY, JobStatus.SYSTEM_ERROR, ], processed=False, )[1], [], { 0: JobStatus.IMAGE_MISSING, 1: JobStatus.AUDIO_MISSING_FOR_DELIVERY, 2: JobStatus.SYSTEM_ERROR, }, [], ), ( jobs_const.JOB_ACTION_CANCEL, get_ids(), get_statuses( statuses=[ JobStatus.IMAGE_MISSING, JobStatus.AUDIO_MISSING_FOR_DELIVERY, JobStatus.SYSTEM_ERROR, ], processed=False, )[1], get_ids(), { 0: JobStatus.IMAGE_MISSING, 1: JobStatus.AUDIO_MISSING_FOR_DELIVERY, 2: JobStatus.SYSTEM_ERROR, }, [], ), ], ) def test_validate( action: str, job_ids: list[int], get_statuses_result: dict[int, JobStatus], get_job_duplicates_result: list[int], expected_result: dict[int, JobStatus], expected_errors: list[dict[str, str | list[int]]], mocker: Mock, ) -> None: """Test _validate function.""" mocked_get_jobs = mocker.patch("vectororder.logic.jobs.job_model.get_jobs") mocked_get_jobs.return_value = get_statuses_result mocked_check_duplicates = mocker.patch( "vectororder.logic.jobs.job_model.get_jobs_duplicates" ) mocked_check_duplicates.return_value = get_job_duplicates_result result, errors = job_logic._validate(action, job_ids) assert result == expected_result assert errors == expected_errors @pytest.mark.parametrize( "action, func_called, validate_result, expected_result", ( ( jobs_const.JOB_ACTION_REENCODE, (True, False, False, False), ({}, []), {}, ), ( jobs_const.JOB_ACTION_REENCODE, (True, True, False, False), (get_statuses(), []), {}, ), ( jobs_const.JOB_ACTION_REDELIVER, (True, False, True, False), (get_statuses(), []), {}, ), ( jobs_const.JOB_ACTION_CANCEL, (True, False, False, True), (get_statuses(), []), {}, ), ( jobs_const.JOB_ACTION_CANCEL, (True, False, False, True), (get_statuses(), []), {}, ), ( jobs_const.JOB_ACTION_CANCEL, (True, False, False, True), (get_statuses(), [{"c": "e"}]), {fields.RESPONSE_ERRORS: [{"c": "e"}]}, ), ( jobs_const.JOB_ACTION_REDELIVER, (True, False, True, False), (get_statuses(), []), {}, ), ( jobs_const.JOB_ACTION_REDELIVER, (True, False, False, False), ({}, []), {}, ), ( jobs_const.JOB_ACTION_REDELIVER, (True, False, False, False), ({}, [{"c": "e"}]), {fields.RESPONSE_ERRORS: [{"c": "e"}]}, ), ), ) def test_perform_status_action( action: str, func_called: tuple[bool, bool, bool, bool], validate_result: tuple[dict[str, str], list[dict[str, str]]], expected_result: dict[str, list[dict[str, str]]], mocker: Mock, ) -> None: """Test perform_status_action function.""" job_ids = get_ids() ( validate_called, reencode_called, redeliver_called, cancel_called, ) = func_called job_statuses = {} valid_ids = [] if validate_result: job_statuses = validate_result[0] valid_ids = list(job_statuses.keys()) mocked_validate = mocker.patch("vectororder.logic.jobs._validate") mocked_validate.return_value = validate_result mocked_set_reencode = mocker.patch("vectororder.logic.jobs.job_model.set_reencode") mocked_set_redeliver = mocker.patch( "vectororder.logic.jobs.job_model.set_redeliver" ) mocked_set_cancel = mocker.patch("vectororder.logic.jobs.job_model.set_cancel") result = job_logic.perform_status_action(action, job_ids) assert mocked_validate.called == validate_called if validate_called: assert mocked_validate.call_args[0] == (action, job_ids) assert mocked_set_reencode.called == reencode_called if reencode_called: assert mocked_set_reencode.call_args[0] == (valid_ids,) assert mocked_set_redeliver.called == redeliver_called if redeliver_called: assert mocked_set_redeliver.call_args[0] == (valid_ids,) assert mocked_set_cancel.called == cancel_called if cancel_called: assert mocked_set_cancel.call_args[0] == (valid_ids,) assert result == expected_result def _make_product(track_types: list[TrackType]) -> Product: return Product( product_id=1, distribution_format_id=1, not_for_distribution="N", context_type="digital", tracks=[ Track( track_id=i, track_type=tt, offer_type=TrackOfferType.ALL, distribution_rights={DownloadStreamRights.DOWNLOAD}, ) for i, tt in enumerate(track_types) ], ) @pytest.mark.parametrize( "test_description, encoding_order_type, track_types, expected_distro", [ ( "RELEASE with no tracks returns 1", EncodingOrderType.RELEASE, [], 1, ), ( "RELEASE with only music tracks returns 1", EncodingOrderType.RELEASE, [TrackType.MUSIC], 1, ), ( "RELEASE with only video tracks returns 3", EncodingOrderType.RELEASE, [TrackType.VIDEO], 3, ), ( "RELEASE with mixed music and video tracks (bundle) returns 1", EncodingOrderType.RELEASE, [TrackType.MUSIC, TrackType.VIDEO], 1, ), ( "RINGTONE with music tracks returns 2", EncodingOrderType.RINGTONE, [TrackType.MUSIC], 2, ), ( "RINGTONE with mixed music and video tracks returns 2", EncodingOrderType.RINGTONE, [TrackType.MUSIC, TrackType.VIDEO], 2, ), ( "RINGTONE with no tracks returns None", EncodingOrderType.RINGTONE, [], None, ), ( "RINGTONE with only video tracks returns None", EncodingOrderType.RINGTONE, [TrackType.VIDEO], None, ), ], ) def test_get_carveouts_distro( test_description: str, encoding_order_type: EncodingOrderType, track_types: list[TrackType], expected_distro: int | None, ) -> None: """Test _get_carveouts_distro.""" product = _make_product(track_types) assert _get_carveouts_distro(encoding_order_type, product) == expected_distro def _test_get_job_eligibility_parameters( *, test_description: str, job_delivery_type: DeliveryType, get_vendor_result: Vendor, get_vendor_expected_calls: list[_Call], get_product_result: Product | None = None, get_product_expected_calls: list[_Call] | None = None, get_store_result: Store | None = None, get_store_expected_calls: list[_Call] | None = None, has_empty_allowed_territories_list_result: tuple[bool, str | None] | None = None, has_empty_allowed_territories_list_expected_calls: list[_Call] | None = None, store_is_hd_only_result: bool | None = None, store_is_hd_only_expected_calls: list[_Call] | None = None, store_has_hd_profile_result: bool | None = None, store_has_hd_profile_expected_calls: list[_Call] | None = None, is_hd_product_result: bool | None = None, is_hd_product_expected_calls: list[_Call] | None = None, delivery_spec_result: DmsDeliverySpec | None = None, delivery_spec_expected_calls: list[_Call] | None = None, has_delivered_job_result: bool | None = None, has_delivered_job_expected_calls: list[_Call] | None = None, has_delivery_history_result: bool | None = None, has_delivery_history_expected_calls: list[_Call] | None = None, expected_result: JobEligibility, ) -> tuple[ str, DeliveryType, Vendor, list[_Call], Product | None, list[_Call] | None, Store | None, list[_Call] | None, tuple[bool, str | None] | None, list[_Call] | None, bool | None, list[_Call] | None, bool | None, list[_Call] | None, bool | None, list[_Call] | None, DmsDeliverySpec | None, list[_Call] | None, bool | None, list[_Call] | None, bool | None, list[_Call] | None, JobEligibility, ]: """Parameters for test_get_job_eligibility.""" return ( test_description, job_delivery_type, get_vendor_result, get_vendor_expected_calls, get_product_result, get_product_expected_calls or [], get_store_result, get_store_expected_calls or [], has_empty_allowed_territories_list_result, has_empty_allowed_territories_list_expected_calls or [], store_is_hd_only_result, store_is_hd_only_expected_calls or [], store_has_hd_profile_result, store_has_hd_profile_expected_calls or [], is_hd_product_result, is_hd_product_expected_calls or [], delivery_spec_result, delivery_spec_expected_calls or [], has_delivered_job_result, has_delivered_job_expected_calls or [], has_delivery_history_result, has_delivery_history_expected_calls or [], expected_result, ) @pytest.mark.parametrize( ( "test_description", "job_delivery_type", "get_vendor_result", "get_vendor_expected_calls", "get_product_result", "get_product_expected_calls", "get_store_result", "get_store_expected_calls", "has_empty_allowed_territories_list_result", "has_empty_allowed_territories_list_expected_calls", "store_is_hd_only_result", "store_is_hd_only_expected_calls", "store_has_hd_profile_result", "store_has_hd_profile_expected_calls", "is_hd_product_result", "is_hd_product_expected_calls", "delivery_spec_result", "delivery_spec_expected_calls", "has_delivered_job_result", "has_delivered_job_expected_calls", "has_delivery_history_result", "has_delivery_history_expected_calls", "expected_result", ), [ _test_get_job_eligibility_parameters( test_description="COMPLETE_ALBUM is not eligible because not for distribution", job_delivery_type=DeliveryType.COMPLETE_ALBUM, get_vendor_result=Vendor( vendor_id=1, name="vendor name", label_identifier="Catalog", api_vendor_id=None, ), get_vendor_expected_calls=[call(199066946312)], get_product_result=Product( product_id=123, distribution_format_id=1, not_for_distribution="NotforFurtherDistribution", context_type="digital", tracks=[], ), get_product_expected_calls=[ call( 199066946312, Store( store_id=286, distribution_feature_ids={ DistributionFeatureId.A_LA_CARTE_DOWNLOAD }, ), ) ], get_store_result=Store( store_id=286, distribution_feature_ids={DistributionFeatureId.A_LA_CARTE_DOWNLOAD}, ), get_store_expected_calls=[call(286)], expected_result=JobEligibility( is_eligible=False, reason=JobIneligibilityReason.NOT_FOR_DISTRIBUTION ), ), _test_get_job_eligibility_parameters( test_description="COMPLETE_ALBUM is not eligible because no track with non-none offer type", job_delivery_type=DeliveryType.COMPLETE_ALBUM, get_vendor_result=Vendor( vendor_id=1, name="vendor name", label_identifier="Catalog", api_vendor_id=None, ), get_vendor_expected_calls=[call(199066946312)], get_product_result=Product( product_id=123, distribution_format_id=1, not_for_distribution="N", context_type="digital", tracks=[ Track( track_id=1, track_type=TrackType.MUSIC, offer_type=TrackOfferType.NONE, distribution_rights=set(), ), Track( track_id=2, track_type=TrackType.MUSIC, offer_type=TrackOfferType.NONE, distribution_rights=set(), ), ], ), get_product_expected_calls=[ call( 199066946312, Store( store_id=286, distribution_feature_ids={ DistributionFeatureId.A_LA_CARTE_DOWNLOAD }, ), ) ], get_store_result=Store( store_id=286, distribution_feature_ids={DistributionFeatureId.A_LA_CARTE_DOWNLOAD}, ), get_store_expected_calls=[call(286)], expected_result=JobEligibility( is_eligible=False, reason=JobIneligibilityReason.NO_TRACK_WITH_OFFER_TYPE, ), ), _test_get_job_eligibility_parameters( test_description="COMPLETE_ALBUM is not eligible because product has empty allowed territories list", job_delivery_type=DeliveryType.COMPLETE_ALBUM, get_vendor_result=Vendor( vendor_id=1, name="vendor name", label_identifier="Catalog", api_vendor_id=None, ), get_vendor_expected_calls=[call(199066946312)], get_product_result=Product( product_id=123, distribution_format_id=1, not_for_distribution="N", context_type="digital", tracks=[ Track( track_id=1, track_type=TrackType.MUSIC, offer_type=TrackOfferType.ALL, distribution_rights={DownloadStreamRights.DOWNLOAD}, ), ], ), get_product_expected_calls=[ call( 199066946312, Store( store_id=286, distribution_feature_ids={ DistributionFeatureId.A_LA_CARTE_DOWNLOAD }, ), ) ], get_store_result=Store( store_id=286, distribution_feature_ids={DistributionFeatureId.A_LA_CARTE_DOWNLOAD}, ), get_store_expected_calls=[call(286)], has_empty_allowed_territories_list_result=( True, "RELEASE_DISTRIBUTION_TYPE_NOT_ALLOWED_CARVEOUT", ), has_empty_allowed_territories_list_expected_calls=[ call(upc=199066946312, store_id=286, distro=1) ], expected_result=JobEligibility( is_eligible=False, reason="RELEASE_DISTRIBUTION_TYPE_NOT_ALLOWED_CARVEOUT", ), ), _test_get_job_eligibility_parameters( test_description="COMPLETE_ALBUM is not eligible because store is hd only and product is not digital audio", job_delivery_type=DeliveryType.COMPLETE_ALBUM, get_vendor_result=Vendor( vendor_id=1, name="vendor name", label_identifier="Catalog", api_vendor_id=None, ), get_vendor_expected_calls=[call(199066946312)], get_product_result=Product( product_id=123, distribution_format_id=57, not_for_distribution="N", context_type="digital", tracks=[ Track( track_id=1, track_type=TrackType.VIDEO, offer_type=TrackOfferType.ALL, distribution_rights={DownloadStreamRights.DOWNLOAD}, ), ], ), get_product_expected_calls=[ call( 199066946312, Store( store_id=286, distribution_feature_ids={ DistributionFeatureId.A_LA_CARTE_DOWNLOAD }, ), ) ], get_store_result=Store( store_id=286, distribution_feature_ids={DistributionFeatureId.A_LA_CARTE_DOWNLOAD}, ), get_store_expected_calls=[call(286)], has_empty_allowed_territories_list_result=(False, None), has_empty_allowed_territories_list_expected_calls=[ call(upc=199066946312, store_id=286, distro=3), ], store_is_hd_only_result=True, store_is_hd_only_expected_calls=[call(286)], expected_result=JobEligibility( is_eligible=False, reason=JobIneligibilityReason.NOT_DIGITAL_AUDIO ), ), _test_get_job_eligibility_parameters( test_description="COMPLETE_ALBUM is not eligible because store is hd only and product is not hd", job_delivery_type=DeliveryType.COMPLETE_ALBUM, get_vendor_result=Vendor( vendor_id=1, name="vendor name", label_identifier="Catalog", api_vendor_id=None, ), get_vendor_expected_calls=[call(199066946312)], get_product_result=Product( product_id=123, distribution_format_id=1, not_for_distribution="N", context_type="digital", tracks=[ Track( track_id=1, track_type=TrackType.MUSIC, offer_type=TrackOfferType.ALL, distribution_rights={DownloadStreamRights.DOWNLOAD}, ), ], ), get_product_expected_calls=[ call( 199066946312, Store( store_id=286, distribution_feature_ids={ DistributionFeatureId.A_LA_CARTE_DOWNLOAD }, ), ) ], get_store_result=Store( store_id=286, distribution_feature_ids={DistributionFeatureId.A_LA_CARTE_DOWNLOAD}, ), get_store_expected_calls=[call(286)], has_empty_allowed_territories_list_result=(False, None), has_empty_allowed_territories_list_expected_calls=[ call(upc=199066946312, store_id=286, distro=1) ], store_is_hd_only_result=True, store_is_hd_only_expected_calls=[call(286)], is_hd_product_result=False, is_hd_product_expected_calls=[call(123)], expected_result=JobEligibility( is_eligible=False, reason=JobIneligibilityReason.NOT_HD ), ), _test_get_job_eligibility_parameters( test_description="COMPLETE_ALBUM is eligible because store is hd only and product is hd", job_delivery_type=DeliveryType.COMPLETE_ALBUM, get_vendor_result=Vendor( vendor_id=1, name="vendor name", label_identifier="Catalog", api_vendor_id=None, ), get_vendor_expected_calls=[call(199066946312)], get_product_result=Product( product_id=123, distribution_format_id=1, not_for_distribution="N", context_type="digital", tracks=[ Track( track_id=1, track_type=TrackType.MUSIC, offer_type=TrackOfferType.ALL, distribution_rights={DownloadStreamRights.DOWNLOAD}, ), ], ), get_product_expected_calls=[ call( 199066946312, Store( store_id=286, distribution_feature_ids={ DistributionFeatureId.A_LA_CARTE_DOWNLOAD }, ), ) ], get_store_result=Store( store_id=286, distribution_feature_ids={DistributionFeatureId.A_LA_CARTE_DOWNLOAD}, ), get_store_expected_calls=[call(286)], has_empty_allowed_territories_list_result=(False, None), has_empty_allowed_territories_list_expected_calls=[ call(upc=199066946312, store_id=286, distro=1) ], store_is_hd_only_result=True, store_is_hd_only_expected_calls=[call(286)], is_hd_product_result=True, is_hd_product_expected_calls=[call(123)], expected_result=JobEligibility(is_eligible=True, reason=None), ), _test_get_job_eligibility_parameters( test_description=( "COMPLETE_ALBUM is not eligible because store is not hd" " but has hd encoding profile and product is not hd" ), job_delivery_type=DeliveryType.COMPLETE_ALBUM, get_vendor_result=Vendor( vendor_id=1, name="vendor name", label_identifier="Catalog", api_vendor_id=None, ), get_vendor_expected_calls=[call(199066946312)], get_product_result=Product( product_id=123, distribution_format_id=1, not_for_distribution="N", context_type="digital", tracks=[ Track( track_id=1, track_type=TrackType.MUSIC, offer_type=TrackOfferType.ALL, distribution_rights={DownloadStreamRights.DOWNLOAD}, ), ], ), get_product_expected_calls=[ call( 199066946312, Store( store_id=286, distribution_feature_ids={ DistributionFeatureId.A_LA_CARTE_DOWNLOAD }, ), ) ], get_store_result=Store( store_id=286, distribution_feature_ids={DistributionFeatureId.A_LA_CARTE_DOWNLOAD}, ), get_store_expected_calls=[call(286)], has_empty_allowed_territories_list_result=(False, None), has_empty_allowed_territories_list_expected_calls=[ call(upc=199066946312, store_id=286, distro=1) ], store_is_hd_only_result=False, store_is_hd_only_expected_calls=[call(286)], store_has_hd_profile_result=True, store_has_hd_profile_expected_calls=[call(286)], is_hd_product_result=False, is_hd_product_expected_calls=[call(123)], expected_result=JobEligibility( is_eligible=False, reason=JobIneligibilityReason.NOT_HD ), ), _test_get_job_eligibility_parameters( test_description="COMPLETE_ALBUM is eligible because store is not hd but has hd profile and product is hd", job_delivery_type=DeliveryType.COMPLETE_ALBUM, get_vendor_result=Vendor( vendor_id=1, name="vendor name", label_identifier="Catalog", api_vendor_id=None, ), get_vendor_expected_calls=[call(199066946312)], get_product_result=Product( product_id=123, distribution_format_id=1, not_for_distribution="N", context_type="digital", tracks=[ Track( track_id=1, track_type=TrackType.MUSIC, offer_type=TrackOfferType.ALL, distribution_rights={DownloadStreamRights.DOWNLOAD}, ), ], ), get_product_expected_calls=[ call( 199066946312, Store( store_id=286, distribution_feature_ids={ DistributionFeatureId.A_LA_CARTE_DOWNLOAD }, ), ) ], get_store_result=Store( store_id=286, distribution_feature_ids={DistributionFeatureId.A_LA_CARTE_DOWNLOAD}, ), get_store_expected_calls=[call(286)], has_empty_allowed_territories_list_result=(False, None), has_empty_allowed_territories_list_expected_calls=[ call(upc=199066946312, store_id=286, distro=1) ], store_is_hd_only_result=False, store_is_hd_only_expected_calls=[call(286)], store_has_hd_profile_result=True, store_has_hd_profile_expected_calls=[call(286)], is_hd_product_result=True, is_hd_product_expected_calls=[call(123)], expected_result=JobEligibility(is_eligible=True, reason=None), ), _test_get_job_eligibility_parameters( test_description="COMPLETE_ALBUM is eligible, store is not hd only and does not have hd profile", job_delivery_type=DeliveryType.COMPLETE_ALBUM, get_vendor_result=Vendor( vendor_id=1, name="vendor name", label_identifier="Catalog", api_vendor_id=None, ), get_vendor_expected_calls=[call(199066946312)], get_product_result=Product( product_id=123, distribution_format_id=1, not_for_distribution="N", context_type="digital", tracks=[ Track( track_id=1, track_type=TrackType.MUSIC, offer_type=TrackOfferType.ALL, distribution_rights={DownloadStreamRights.DOWNLOAD}, ), ], ), get_product_expected_calls=[ call( 199066946312, Store( store_id=286, distribution_feature_ids={ DistributionFeatureId.A_LA_CARTE_DOWNLOAD }, ), ) ], get_store_result=Store( store_id=286, distribution_feature_ids={DistributionFeatureId.A_LA_CARTE_DOWNLOAD}, ), get_store_expected_calls=[call(286)], has_empty_allowed_territories_list_result=(False, None), has_empty_allowed_territories_list_expected_calls=[ call(upc=199066946312, store_id=286, distro=1) ], store_is_hd_only_result=False, store_is_hd_only_expected_calls=[call(286)], store_has_hd_profile_result=False, store_has_hd_profile_expected_calls=[call(286)], expected_result=JobEligibility(is_eligible=True, reason=None), ), _test_get_job_eligibility_parameters( test_description="METADATA_UPDATE is not eligible, not previously delivered", job_delivery_type=DeliveryType.METADATA_UPDATE, get_vendor_result=Vendor( vendor_id=1, name="vendor name", label_identifier="Catalog", api_vendor_id=None, ), get_vendor_expected_calls=[call(199066946312)], get_store_result=Store( store_id=286, distribution_feature_ids={DistributionFeatureId.A_LA_CARTE_DOWNLOAD}, ), get_store_expected_calls=[call(286)], get_product_result=Product( product_id=123, distribution_format_id=1, not_for_distribution="N", context_type="digital", tracks=[], ), get_product_expected_calls=[ call( 199066946312, Store( store_id=286, distribution_feature_ids={ DistributionFeatureId.A_LA_CARTE_DOWNLOAD }, ), ) ], delivery_spec_result=DmsDeliverySpec( dms_delivery_spec_id=1, dms_master_master_id=1, order_type=EncodingOrderType.RELEASE, encoding="Y", delivery="Y", metadata_update=True, ), delivery_spec_expected_calls=[call(286, EncodingOrderType.RELEASE)], has_delivered_job_result=False, has_delivered_job_expected_calls=[call(upc=199066946312, store_id=286)], has_delivery_history_result=False, has_delivery_history_expected_calls=[call(upc=199066946312, store_id=286)], expected_result=JobEligibility( is_eligible=False, reason=JobIneligibilityReason.NOT_PREVIOUSLY_DELIVERED, ), ), _test_get_job_eligibility_parameters( test_description="METADATA_UPDATE is not eligible because updates disabled for dms", job_delivery_type=DeliveryType.METADATA_UPDATE, get_vendor_result=Vendor( vendor_id=1, name="vendor name", label_identifier="Catalog", api_vendor_id=None, ), get_vendor_expected_calls=[call(199066946312)], get_store_result=Store( store_id=286, distribution_feature_ids={DistributionFeatureId.A_LA_CARTE_DOWNLOAD}, ), get_store_expected_calls=[call(286)], get_product_result=Product( product_id=123, distribution_format_id=1, not_for_distribution="N", context_type="digital", tracks=[], ), get_product_expected_calls=[ call( 199066946312, Store( store_id=286, distribution_feature_ids={ DistributionFeatureId.A_LA_CARTE_DOWNLOAD }, ), ) ], delivery_spec_result=DmsDeliverySpec( dms_delivery_spec_id=1, dms_master_master_id=1, order_type=EncodingOrderType.RELEASE, encoding="Y", delivery="Y", metadata_update=False, ), delivery_spec_expected_calls=[call(286, EncodingOrderType.RELEASE)], expected_result=JobEligibility( is_eligible=False, reason=JobIneligibilityReason.DMS_NO_METADATA_UPDATES ), ), _test_get_job_eligibility_parameters( test_description="METADATA_UPDATE is eligible because delivered job exists", job_delivery_type=DeliveryType.METADATA_UPDATE, get_vendor_result=Vendor( vendor_id=1, name="vendor name", label_identifier="Catalog", api_vendor_id=None, ), get_vendor_expected_calls=[call(199066946312)], get_store_result=Store( store_id=286, distribution_feature_ids={DistributionFeatureId.A_LA_CARTE_DOWNLOAD}, ), get_store_expected_calls=[call(286)], get_product_result=Product( product_id=123, distribution_format_id=1, not_for_distribution="N", context_type="digital", tracks=[], ), get_product_expected_calls=[ call( 199066946312, Store( store_id=286, distribution_feature_ids={ DistributionFeatureId.A_LA_CARTE_DOWNLOAD }, ), ) ], delivery_spec_result=DmsDeliverySpec( dms_delivery_spec_id=1, dms_master_master_id=1, order_type=EncodingOrderType.RELEASE, encoding="Y", delivery="Y", metadata_update=True, ), delivery_spec_expected_calls=[call(286, EncodingOrderType.RELEASE)], has_delivered_job_result=True, has_delivered_job_expected_calls=[call(upc=199066946312, store_id=286)], expected_result=JobEligibility(is_eligible=True, reason=None), ), _test_get_job_eligibility_parameters( test_description="METADATA_UPDATE is eligible because delivery history exists", job_delivery_type=DeliveryType.METADATA_UPDATE, get_vendor_result=Vendor( vendor_id=1, name="vendor name", label_identifier="Catalog", api_vendor_id=None, ), get_vendor_expected_calls=[call(199066946312)], get_store_result=Store( store_id=286, distribution_feature_ids={DistributionFeatureId.A_LA_CARTE_DOWNLOAD}, ), get_store_expected_calls=[call(286)], get_product_result=Product( product_id=123, distribution_format_id=1, not_for_distribution="N", context_type="digital", tracks=[], ), get_product_expected_calls=[ call( 199066946312, Store( store_id=286, distribution_feature_ids={ DistributionFeatureId.A_LA_CARTE_DOWNLOAD }, ), ) ], delivery_spec_result=DmsDeliverySpec( dms_delivery_spec_id=1, dms_master_master_id=1, order_type=EncodingOrderType.RELEASE, encoding="Y", delivery="Y", metadata_update=True, ), delivery_spec_expected_calls=[call(286, EncodingOrderType.RELEASE)], has_delivered_job_result=False, has_delivered_job_expected_calls=[call(upc=199066946312, store_id=286)], has_delivery_history_result=True, has_delivery_history_expected_calls=[call(upc=199066946312, store_id=286)], expected_result=JobEligibility(is_eligible=True, reason=None), ), _test_get_job_eligibility_parameters( test_description="COMPLETE_ALBUM is not eligible because vendor is API", job_delivery_type=DeliveryType.COMPLETE_ALBUM, get_vendor_result=Vendor( vendor_id=1, name="vendor name", label_identifier="Catalog", api_vendor_id=1, ), get_vendor_expected_calls=[call(199066946312)], get_store_result=Store( store_id=286, distribution_feature_ids={DistributionFeatureId.A_LA_CARTE_DOWNLOAD}, ), get_store_expected_calls=[call(286)], get_product_result=Product( product_id=123, distribution_format_id=1, not_for_distribution="N", context_type="digital", tracks=[], ), get_product_expected_calls=[ call( 199066946312, Store( store_id=286, distribution_feature_ids={ DistributionFeatureId.A_LA_CARTE_DOWNLOAD }, ), ) ], expected_result=JobEligibility( is_eligible=False, reason=JobIneligibilityReason.VENDOR_IS_API ), ), _test_get_job_eligibility_parameters( test_description="COMPLETE_ALBUM is not eligible, track offer type mismatch store features", job_delivery_type=DeliveryType.COMPLETE_ALBUM, get_vendor_result=Vendor( vendor_id=1, name="vendor name", label_identifier="Catalog", api_vendor_id=None, ), get_vendor_expected_calls=[call(199066946312)], get_product_result=Product( product_id=123, distribution_format_id=1, not_for_distribution="N", context_type="digital", tracks=[ Track( track_id=1, track_type=TrackType.MUSIC, offer_type=TrackOfferType.ALL, distribution_rights=set(), ), ], ), get_product_expected_calls=[ call( 199066946312, Store( store_id=286, distribution_feature_ids={DistributionFeatureId.RINGBACK_TONES}, ), ) ], get_store_result=Store( store_id=286, distribution_feature_ids={DistributionFeatureId.RINGBACK_TONES}, ), get_store_expected_calls=[call(286)], expected_result=JobEligibility( is_eligible=False, reason=JobIneligibilityReason.TRACK_OFFER_TYPE_MISMATCH_SERVICE_DISTRIBUTION_FEATURES, ), ), _test_get_job_eligibility_parameters( test_description="COMPLETE_ALBUM is eligible, physical products does not have tracks", job_delivery_type=DeliveryType.COMPLETE_ALBUM, get_vendor_result=Vendor( vendor_id=1, name="vendor name", label_identifier="Catalog", api_vendor_id=None, ), get_vendor_expected_calls=[call(199066946312)], get_product_result=Product( product_id=123, distribution_format_id=70, not_for_distribution="N", context_type="physical", tracks=[], ), get_product_expected_calls=[ call( 199066946312, Store( store_id=286, distribution_feature_ids={ DistributionFeatureId.A_LA_CARTE_DOWNLOAD }, is_physical=True, ), ) ], get_store_result=Store( store_id=286, distribution_feature_ids={DistributionFeatureId.A_LA_CARTE_DOWNLOAD}, is_physical=True, ), get_store_expected_calls=[call(286)], has_empty_allowed_territories_list_result=(False, None), has_empty_allowed_territories_list_expected_calls=[ call(upc=199066946312, store_id=286, distro=1) ], store_is_hd_only_result=False, store_is_hd_only_expected_calls=[call(286)], store_has_hd_profile_result=False, store_has_hd_profile_expected_calls=[call(286)], expected_result=JobEligibility(is_eligible=True, reason=None), ), _test_get_job_eligibility_parameters( test_description="COMPLETE_ALBUM is not eligible because physical product has empty allowed territories list", job_delivery_type=DeliveryType.COMPLETE_ALBUM, get_vendor_result=Vendor( vendor_id=1, name="vendor name", label_identifier="Catalog", api_vendor_id=None, ), get_vendor_expected_calls=[call(199066946312)], get_product_result=Product( product_id=123, distribution_format_id=70, not_for_distribution="N", context_type="physical", tracks=[ Track( track_id=1, track_type=TrackType.MUSIC, offer_type=TrackOfferType.ALL, distribution_rights={DownloadStreamRights.DOWNLOAD}, ), ], ), get_product_expected_calls=[ call( 199066946312, Store( store_id=286, distribution_feature_ids={ DistributionFeatureId.A_LA_CARTE_DOWNLOAD }, is_physical=True, ), ) ], get_store_result=Store( store_id=286, distribution_feature_ids={DistributionFeatureId.A_LA_CARTE_DOWNLOAD}, is_physical=True, ), get_store_expected_calls=[call(286)], has_empty_allowed_territories_list_result=( True, "RELEASE_DISTRIBUTION_TYPE_NOT_ALLOWED_CARVEOUT", ), has_empty_allowed_territories_list_expected_calls=[ call(upc=199066946312, store_id=286, distro=1) ], expected_result=JobEligibility( is_eligible=False, reason="RELEASE_DISTRIBUTION_TYPE_NOT_ALLOWED_CARVEOUT", ), ), _test_get_job_eligibility_parameters( test_description=( "COMPLETE_ALBUM is not eligible because store is not physical" ), job_delivery_type=DeliveryType.COMPLETE_ALBUM, get_vendor_result=Vendor( vendor_id=1, name="vendor name", label_identifier="Catalog", api_vendor_id=None, ), get_vendor_expected_calls=[call(199066946312)], get_product_result=Product( product_id=123, distribution_format_id=70, not_for_distribution="N", context_type="physical", tracks=[ Track( track_id=1, track_type=TrackType.MUSIC, offer_type=TrackOfferType.ALL, distribution_rights={DownloadStreamRights.DOWNLOAD}, ), ], ), get_product_expected_calls=[ call( 199066946312, Store( store_id=286, distribution_feature_ids={ DistributionFeatureId.A_LA_CARTE_DOWNLOAD }, ), ) ], get_store_result=Store( store_id=286, distribution_feature_ids={DistributionFeatureId.A_LA_CARTE_DOWNLOAD}, ), get_store_expected_calls=[call(286)], has_empty_allowed_territories_list_result=( False, None, ), has_empty_allowed_territories_list_expected_calls=[], expected_result=JobEligibility( is_eligible=False, reason=JobIneligibilityReason.PHYSICAL_RELEASE, ), ), ], ) def test_get_job_eligibility( test_description: str, job_delivery_type: DeliveryType, get_vendor_result: Vendor, get_vendor_expected_calls: list[_Call], get_product_result: Product | None, get_product_expected_calls: list[_Call] | None, get_store_result: Store | None, get_store_expected_calls: list[_Call] | None, has_empty_allowed_territories_list_result: tuple[bool, str | None] | None, has_empty_allowed_territories_list_expected_calls: list[_Call] | None, store_is_hd_only_result: bool | None, store_is_hd_only_expected_calls: list[_Call] | None, store_has_hd_profile_result: bool | None, store_has_hd_profile_expected_calls: list[_Call] | None, is_hd_product_result: bool | None, is_hd_product_expected_calls: list[_Call] | None, delivery_spec_result: DmsDeliverySpec | None, delivery_spec_expected_calls: list[_Call] | None, has_delivered_job_result: bool | None, has_delivered_job_expected_calls: list[_Call] | None, has_delivery_history_result: bool | None, has_delivery_history_expected_calls: list[_Call] | None, expected_result: JobEligibility, mocker: Mock, ) -> None: """Test get_job_eligibility success.""" job_id = 245 get_job_mock = mocker.patch( "vectororder.models.job_model.get_job", return_value=Job( job_id=job_id, upc=199066946312, store_id=286, delivery_type=job_delivery_type, encoding_order_type=EncodingOrderType.RELEASE, status=JobStatus.READY_TO_ENCODE, ), ) get_vendor_mock = mocker.patch( "vectororder.models.vendor.get_vendor", return_value=get_vendor_result ) get_product_mock = mocker.patch( "vectororder.models.product_model.get_product", return_value=get_product_result ) has_empty_allowed_territories_list_mock = mocker.patch( "vectororder.models.carveouts_model.has_empty_allowed_territories_list", return_value=has_empty_allowed_territories_list_result, ) store_is_hd_only_mock = mocker.patch( "vectororder.models.store_model.is_hd_only", return_value=store_is_hd_only_result, ) is_hd_product_mock = mocker.patch( "vectororder.models.assets_model.is_hd_product", return_value=is_hd_product_result, ) store_has_hd_profile_mock = mocker.patch( "vectororder.models.store_model.has_hd_encoding_profile", return_value=store_has_hd_profile_result, ) has_delivered_job_mock = mocker.patch( "vectororder.models.job_model.has_delivered_job", return_value=has_delivered_job_result, ) has_delivery_history_mock = mocker.patch( "vectororder.models.job_model.has_delivery_history", return_value=has_delivery_history_result, ) get_store_mock = mocker.patch( "vectororder.models.store_model.get_store", return_value=get_store_result ) get_delivery_spec_mock = mocker.patch( "vectororder.models.dms_delivery_spec.get_dms_delivery_spec", return_value=delivery_spec_result, ) result = job_logic.get_job_eligibility(job_id) assert get_job_mock.mock_calls == [call(job_id)] assert get_vendor_mock.mock_calls == get_vendor_expected_calls assert get_product_mock.mock_calls == get_product_expected_calls assert ( has_empty_allowed_territories_list_mock.mock_calls == has_empty_allowed_territories_list_expected_calls ) assert store_is_hd_only_mock.mock_calls == store_is_hd_only_expected_calls assert store_has_hd_profile_mock.mock_calls == store_has_hd_profile_expected_calls assert is_hd_product_mock.mock_calls == is_hd_product_expected_calls assert get_delivery_spec_mock.mock_calls == delivery_spec_expected_calls assert has_delivered_job_mock.mock_calls == has_delivered_job_expected_calls assert has_delivery_history_mock.mock_calls == has_delivery_history_expected_calls assert get_store_mock.mock_calls == get_store_expected_calls assert result == expected_result @pytest.mark.parametrize( "test_description, get_product_side_effect, get_vendor_side_effect, expected_result", [ ( "not eligible because product not found", ProductNotFoundError, None, JobEligibility( is_eligible=False, reason=JobIneligibilityReason.PRODUCT_NOT_FOUND ), ), ( "not eligible because product not in content", ProductNotInContent, None, JobEligibility( is_eligible=False, reason=JobIneligibilityReason.RELEASE_NOT_IN_CONTENT ), ), ( "not eligible because vendor not found", None, VendorNotFoundError, JobEligibility( is_eligible=False, reason=JobIneligibilityReason.VENDOR_NOT_FOUND ), ), ], ) def test_get_job_eligibility_model_exceptions( test_description: str, get_product_side_effect: type[Exception] | None, get_vendor_side_effect: type[Exception] | None, expected_result: JobEligibility, mocker: Mock, ) -> None: """Test get_job_eligibility returns ineligible when models raise known exceptions.""" job_id = 245 mocker.patch( "vectororder.models.job_model.get_job", return_value=Job( job_id=job_id, upc=199066946312, store_id=286, delivery_type=DeliveryType.COMPLETE_ALBUM, encoding_order_type=EncodingOrderType.RELEASE, status=JobStatus.READY_TO_ENCODE, ), ) mocker.patch( "vectororder.models.store_model.get_store", return_value=Store( store_id=286, distribution_feature_ids={DistributionFeatureId.A_LA_CARTE_DOWNLOAD}, ), ) mocker.patch( "vectororder.models.product_model.get_product", side_effect=get_product_side_effect, return_value=Product( product_id=123, distribution_format_id=1, not_for_distribution="N", context_type="digital", tracks=[], ) if get_product_side_effect is None else None, ) mocker.patch( "vectororder.models.vendor.get_vendor", side_effect=get_vendor_side_effect, return_value=Vendor( vendor_id=1, name="vendor name", label_identifier="Catalog", api_vendor_id=None, ) if get_vendor_side_effect is None else None, ) result = job_logic.get_job_eligibility(job_id) assert result == expected_result, test_description @pytest.mark.parametrize( "test_description, delivery_type, track_types", [ ( "COMPLETE_ALBUM RINGTONE with only video tracks is ineligible", DeliveryType.COMPLETE_ALBUM, [TrackType.VIDEO], ), ( "METADATA_UPDATE RINGTONE with only video tracks is ineligible", DeliveryType.METADATA_UPDATE, [TrackType.VIDEO], ), ], ) def test_get_job_eligibility_ringtone_product_type_mismatch( test_description: str, delivery_type: DeliveryType, track_types: list[TrackType], mocker: Mock, ) -> None: """Test RINGTONE order type without music tracks returns PRODUCT_TYPE_ORDER_TYPE_MISMATCH regardless of delivery type.""" job_id = 245 mocker.patch( "vectororder.models.job_model.get_job", return_value=Job( job_id=job_id, upc=199066946312, store_id=286, delivery_type=delivery_type, encoding_order_type=EncodingOrderType.RINGTONE, status=JobStatus.READY_TO_ENCODE, ), ) mocker.patch( "vectororder.models.store_model.get_store", return_value=Store( store_id=286, distribution_feature_ids={DistributionFeatureId.A_LA_CARTE_DOWNLOAD}, ), ) mocker.patch( "vectororder.models.vendor.get_vendor", return_value=Vendor( vendor_id=1, name="vendor name", label_identifier="Catalog", api_vendor_id=None, ), ) mocker.patch( "vectororder.models.product_model.get_product", return_value=_make_product(track_types), ) result = job_logic.get_job_eligibility(job_id) assert result == JobEligibility( is_eligible=False, reason=JobIneligibilityReason.PRODUCT_TYPE_ORDER_TYPE_MISMATCH, ), test_description def test_get_job_eligibility_ringtone_complete_album_calls_distro_2( mocker: Mock, ) -> None: """Test RINGTONE + music tracks calls has_empty_allowed_territories_list with distro=2.""" job_id = 245 mocker.patch( "vectororder.models.job_model.get_job", return_value=Job( job_id=job_id, upc=199066946312, store_id=286, delivery_type=DeliveryType.COMPLETE_ALBUM, encoding_order_type=EncodingOrderType.RINGTONE, status=JobStatus.READY_TO_ENCODE, ), ) mocker.patch( "vectororder.models.store_model.get_store", return_value=Store( store_id=286, distribution_feature_ids={DistributionFeatureId.A_LA_CARTE_DOWNLOAD}, ), ) mocker.patch( "vectororder.models.vendor.get_vendor", return_value=Vendor( vendor_id=1, name="vendor name", label_identifier="Catalog", api_vendor_id=None, ), ) mocker.patch( "vectororder.models.product_model.get_product", return_value=_make_product([TrackType.MUSIC]), ) carveouts_mock = mocker.patch( "vectororder.models.carveouts_model.has_empty_allowed_territories_list", return_value=(False, None), ) mocker.patch("vectororder.models.store_model.is_hd_only", return_value=False) mocker.patch( "vectororder.models.store_model.has_hd_encoding_profile", return_value=False ) result = job_logic.get_job_eligibility(job_id) assert carveouts_mock.mock_calls == [call(upc=199066946312, store_id=286, distro=2)] assert result == JobEligibility(is_eligible=True, reason=None) def test_get_job_delivery_xml_payload_happy_path(mocker: Mock) -> None: job_id = 245 job = Job( job_id=job_id, upc=199066946312, store_id=286, delivery_type=DeliveryType.COMPLETE_ALBUM, encoding_order_type=EncodingOrderType.RELEASE, status=JobStatus.DELIVERED, ) get_job_mock = mocker.patch( "vectororder.models.job_model.get_job", return_value=job, ) expected_path = f"metadata/{_md5_job_id(job_id)}-0.xml" expected_url = "https://presigned.example.com/delivery.xml?sig=abc" check_exists_mock = mocker.patch.object( s3_file, "check_s3_file_exists", return_value=True ) get_storage_mock = mocker.patch.object( s3_file, "get_s3_file_storage_class", return_value="STANDARD" ) create_url_mock = mocker.patch.object( s3_file, "create_presigned_url", return_value=expected_url ) payload = get_job_delivery_xml_payload(job_id) assert get_job_mock.mock_calls == [call(job_id)] assert check_exists_mock.mock_calls == [call("dev-vector-audit", expected_path)] assert get_storage_mock.mock_calls == [call("dev-vector-audit", expected_path)] assert create_url_mock.mock_calls == [ call("dev-vector-audit", expected_path, 300, "attachment;") ] assert payload.presigned_link == expected_url @pytest.mark.parametrize( ( "test_description", "job_status", "s3_exists", "storage_class", "expected_exception", "expected_message_contains", "expected_check_exists_calls", "expected_get_storage_calls", "expected_create_url_calls", ), [ ( "raises when job is not completed", JobStatus.READY_TO_ENCODE, None, None, DeliveryFileNotFoundError, "XML is available only for completed statuses", [], [], [], ), ( "raises when S3 object is missing", JobStatus.DELIVERED, False, None, DeliveryFileNotFoundError, "Delivery XML file does not exist", [call("dev-vector-audit", None)], [], [], ), ( "raises when S3 object in GLACIER", JobStatus.DELIVERED, True, "GLACIER", DeliveryFileNotAvailableError, "requires restoration", [call("dev-vector-audit", None)], [call("dev-vector-audit", None)], [], ), ( "raises when S3 object in DEEP_ARCHIVE", JobStatus.DELIVERED, True, "DEEP_ARCHIVE", DeliveryFileNotAvailableError, "requires restoration", [call("dev-vector-audit", None)], [call("dev-vector-audit", None)], [], ), ], ) def test_get_job_delivery_xml_payload_errors_parametrized( mocker: Mock, test_description: str, job_status: JobStatus, s3_exists: bool | None, storage_class: str | None, expected_exception: type[BaseException], expected_message_contains: str, expected_check_exists_calls: list[_Call], expected_get_storage_calls: list[_Call], expected_create_url_calls: list[_Call], ) -> None: job_id = 245 job = Job( job_id=job_id, upc=199066946312, store_id=286, delivery_type=DeliveryType.COMPLETE_ALBUM, encoding_order_type=EncodingOrderType.RELEASE, status=job_status, ) get_job_mock = mocker.patch( "vectororder.models.job_model.get_job", return_value=job, ) check_exists_mock = mocker.patch.object( s3_file, "check_s3_file_exists", return_value=(s3_exists if s3_exists is not None else False), ) get_storage_mock = mocker.patch.object( s3_file, "get_s3_file_storage_class", return_value=(storage_class if storage_class is not None else "STANDARD"), ) create_url_mock = mocker.patch.object( s3_file, "create_presigned_url", return_value="" ) expected_path = f"metadata/{_md5_job_id(job_id)}-0.xml" expected_check_exists_calls = set_path_for_s3_calls( expected_check_exists_calls, expected_path ) expected_get_storage_calls = set_path_for_s3_calls( expected_get_storage_calls, expected_path ) expected_create_url_calls = set_path_for_s3_calls( expected_create_url_calls, expected_path ) with pytest.raises(expected_exception) as excinfo: get_job_delivery_xml_payload(job_id) assert get_job_mock.mock_calls == [call(job_id)], test_description assert check_exists_mock.mock_calls == expected_check_exists_calls, test_description assert get_storage_mock.mock_calls == expected_get_storage_calls, test_description assert create_url_mock.mock_calls == expected_create_url_calls, test_description assert expected_message_contains in str(excinfo.value), test_description def _md5_job_id(job_id: int) -> str: return hashlib.md5(str(job_id).encode()).hexdigest() def set_path_for_s3_calls(calls: list[_Call], expected_path: str) -> list[_Call]: out = [] for c in calls: args = list(c.args) args[1] = expected_path out.append(call(*args, **c.kwargs)) return out