"""Tests for product transfer job logic (PORT-67).""" from unittest.mock import patch from oto import response from project_manager.logic import project_transfer as logic _IDENTITY_UUID = '10436b38-5e11-472d-b6a4-bf1ee2b1b438' def _job(job_id=1, status='QUEUED', originating_vendor_id=100, destination_vendor_id=200, deleted_at=None, destination_artist_id=None, destination_subaccount_id=None): return { 'project_transfer_job_id': job_id, 'project_id': 10, 'originating_vendor_id': originating_vendor_id, 'originating_subaccount_id': None, 'originating_artist_id': None, 'destination_vendor_id': destination_vendor_id, 'destination_subaccount_id': destination_subaccount_id, 'destination_artist_id': destination_artist_id, 'status': status, 'created_by_identity_id': _IDENTITY_UUID, 'created_at': '2026-05-01T00:00:00', 'revenue_cutoff_date': None, 'transfer_completed_on': None, 'sfn_execution_arn': None, 'failure_reason': None, 'last_updated_at': None, 'last_updated_by_identity_id': _IDENTITY_UUID, 'executed_by_identity_id': None, 'deleted_at': deleted_at, 'deleted_by_identity_id': None, } def _product(release_id=10, source_artist_id=None, destination_artist_id=None): return { 'product_transfer_history_id': 1, 'project_transfer_job_id': 1, 'release_id': release_id, 'source_artist_id': source_artist_id, 'destination_artist_id': destination_artist_id, 'source_video_artist_id': None, 'destination_video_artist_id': None, 'deleted_at': None, 'deleted_by_identity_id': None, } _VALID_BODY = { 'project_id': 10, 'destination_vendor_id': 200, } _RESOLVED_ORIGIN = { 'originating_vendor_id': 100, 'originating_subaccount_id': None, 'originating_artist_id': None, } _RELEASES = [{'release_id': 10, 'source_artist_id': 555}] def test_list_returns_items_and_total(): """Returns items and total_count in the response message.""" with patch('project_manager.logic.project_transfer.job_model') as m: m.get_transfer_jobs.return_value = ([_job()], 1) result = logic.list_transfer_jobs({}, _IDENTITY_UUID) assert result.status == 200 assert result.message['total_count'] == 1 assert len(result.message['items']) == 1 def test_list_passes_filters_to_model(): """Parsed query params are forwarded as keyword args to get_transfer_jobs.""" with patch('project_manager.logic.project_transfer.job_model') as m: m.get_transfer_jobs.return_value = ([], 0) logic.list_transfer_jobs( {'status': 'QUEUED', 'originating_vendor_id': '5', 'limit': '10', 'offset': '0'}, _IDENTITY_UUID, ) m.get_transfer_jobs.assert_called_once_with( status='QUEUED', originating_vendor_id=5, destination_vendor_id=None, project_id=None, limit=10, offset=0, ) def test_list_returns_400_on_invalid_limit(): """Non-integer limit returns a 400 validation error.""" result = logic.list_transfer_jobs({'limit': 'bad'}, _IDENTITY_UUID) assert result.status == 400 def test_get_returns_job(): """Detail response returns the job dict (products are a separate endpoint).""" with patch('project_manager.logic.project_transfer.job_model') as jm: jm.get_transfer_job.return_value = _job() result = logic.get_transfer_job(1) assert result.status == 200 assert result.message['project_transfer_job_id'] == 1 assert 'products' not in result.message def test_get_returns_404_when_not_found(): """Returns 404 when no job matches the given ID.""" with patch('project_manager.logic.project_transfer.job_model') as jm: jm.get_transfer_job.return_value = None result = logic.get_transfer_job(999) assert result.status == 404 def test_fetch_job_for_auth_returns_job_dict(): """Returns the bare job dict for ownership checks.""" with patch('project_manager.logic.project_transfer.job_model') as jm: jm.get_transfer_job.return_value = _job() result = logic.fetch_job_for_auth(1) assert isinstance(result, dict) assert result['project_transfer_job_id'] == 1 def test_fetch_job_for_auth_returns_404_when_not_found(): """Returns 404 Response when no job matches.""" with patch('project_manager.logic.project_transfer.job_model') as jm: jm.get_transfer_job.return_value = None result = logic.fetch_job_for_auth(999) assert isinstance(result, response.Response) assert result.status == 404 def test_create_snapshots_releases_with_source_artist_only(): """Bulk-inserts product_transfer_history rows with destination_artist_id NULL.""" with patch('project_manager.logic.project_transfer.job_model') as jm, \ patch('project_manager.logic.project_transfer.history_model') as hm: jm.get_releases_for_project.return_value = _RELEASES jm.create_transfer_job.return_value = _job() hm.snapshot_releases_for_job.return_value = [_product(source_artist_id=555)] result = logic.create_transfer_job( _VALID_BODY, _IDENTITY_UUID, **_RESOLVED_ORIGIN) assert result.status == 201 assert 'products' not in result.message jm.get_releases_for_project.assert_called_once_with(10) hm.snapshot_releases_for_job.assert_called_once_with(1, _RELEASES) def test_create_returns_400_when_required_field_missing(): """Missing destination_vendor_id returns 400.""" body = {k: v for k, v in _VALID_BODY.items() if k != 'destination_vendor_id'} result = logic.create_transfer_job(body, _IDENTITY_UUID, **_RESOLVED_ORIGIN) assert result.status == 400 def test_create_sets_revenue_cutoff_date_to_last_day_of_previous_month(): """revenue_cutoff_date is always the last day of the previous month, computed server-side.""" import datetime fake_today = datetime.date(2026, 5, 12) with patch('project_manager.logic.project_transfer.job_model') as jm, \ patch('project_manager.logic.project_transfer.history_model') as hm, \ patch('project_manager.logic.project_transfer.datetime') as mock_dt: mock_dt.date.today.return_value = fake_today mock_dt.date.side_effect = datetime.date mock_dt.timedelta = datetime.timedelta jm.get_releases_for_project.return_value = _RELEASES jm.create_transfer_job.return_value = _job() hm.snapshot_releases_for_job.return_value = [_product(source_artist_id=555)] logic.create_transfer_job(_VALID_BODY, _IDENTITY_UUID, **_RESOLVED_ORIGIN) passed = jm.create_transfer_job.call_args[0][0] assert passed['revenue_cutoff_date'] == datetime.date(2026, 4, 30) def test_create_forwards_originating_artist_id_to_model(): """originating_artist_id resolved by handler is forwarded to the model.""" with patch('project_manager.logic.project_transfer.job_model') as jm, \ patch('project_manager.logic.project_transfer.history_model') as hm: jm.get_releases_for_project.return_value = _RELEASES jm.create_transfer_job.return_value = _job() hm.snapshot_releases_for_job.return_value = [_product(source_artist_id=555)] logic.create_transfer_job( _VALID_BODY, _IDENTITY_UUID, originating_vendor_id=100, originating_subaccount_id=None, originating_artist_id=42, ) passed = jm.create_transfer_job.call_args[0][0] assert passed['originating_artist_id'] == 42 def test_create_ignores_client_provided_revenue_cutoff_date(): """Client-provided revenue_cutoff_date is ignored; server always computes it.""" import datetime fake_today = datetime.date(2026, 5, 12) with patch('project_manager.logic.project_transfer.job_model') as jm, \ patch('project_manager.logic.project_transfer.history_model') as hm, \ patch('project_manager.logic.project_transfer.datetime') as mock_dt: mock_dt.date.today.return_value = fake_today mock_dt.date.side_effect = datetime.date mock_dt.timedelta = datetime.timedelta jm.get_releases_for_project.return_value = _RELEASES jm.create_transfer_job.return_value = _job() hm.snapshot_releases_for_job.return_value = [_product(source_artist_id=555)] logic.create_transfer_job( {**_VALID_BODY, 'revenue_cutoff_date': '2099-12-31'}, _IDENTITY_UUID, **_RESOLVED_ORIGIN, ) passed = jm.create_transfer_job.call_args[0][0] assert passed['revenue_cutoff_date'] == datetime.date(2026, 4, 30) def test_create_returns_400_when_no_releases_found(): """Returns 400 when the project has no releases (active or deleted).""" with patch('project_manager.logic.project_transfer.job_model') as jm: jm.get_releases_for_project.return_value = [] result = logic.create_transfer_job( _VALID_BODY, _IDENTITY_UUID, **_RESOLVED_ORIGIN) assert result.status == 400 def test_set_destination_artists_bulk_updates_rows(): """Bulk-sets destination_artist_id for multiple releases in one call.""" updates = [ {'release_id': 10, 'destination_artist_id': 777}, {'release_id': 11, 'destination_artist_id': 888}, ] with patch('project_manager.logic.project_transfer.job_model') as jm, \ patch('project_manager.logic.project_transfer.history_model') as hm: jm.get_transfer_job.return_value = _job() hm.set_destination_artists.return_value = ( [_product(release_id=10, destination_artist_id=777), _product(release_id=11, destination_artist_id=888)], [], ) result = logic.set_destination_artists(1, {'updates': updates}) assert result.status == 200 assert result.message['updated_count'] == 2 # Logic layer always passes destination_video_artist_id (None when absent) parsed_updates = [ {'release_id': 10, 'destination_artist_id': 777, 'destination_video_artist_id': None}, {'release_id': 11, 'destination_artist_id': 888, 'destination_video_artist_id': None}, ] hm.set_destination_artists.assert_called_once_with(1, parsed_updates) def test_set_destination_artists_passes_video_artist_id(): """destination_video_artist_id is forwarded to the model when provided.""" updates = [ {'release_id': 10, 'destination_artist_id': 777, 'destination_video_artist_id': 900}, ] with patch('project_manager.logic.project_transfer.job_model') as jm, \ patch('project_manager.logic.project_transfer.history_model') as hm: jm.get_transfer_job.return_value = _job() hm.set_destination_artists.return_value = ( [_product(release_id=10, destination_artist_id=777)], [], ) result = logic.set_destination_artists(1, {'updates': updates}) assert result.status == 200 parsed_updates = [ {'release_id': 10, 'destination_artist_id': 777, 'destination_video_artist_id': 900}, ] hm.set_destination_artists.assert_called_once_with(1, parsed_updates) def test_set_destination_artists_404_when_job_missing(): """Returns 404 when the parent job does not exist.""" with patch('project_manager.logic.project_transfer.job_model') as jm: jm.get_transfer_job.return_value = None result = logic.set_destination_artists(999, {'updates': [ {'release_id': 10, 'destination_artist_id': 777}, ]}) assert result.status == 404 def test_set_destination_artists_404_when_any_release_missing(): """Returns 404 listing missing release_ids; whole batch is rejected.""" with patch('project_manager.logic.project_transfer.job_model') as jm, \ patch('project_manager.logic.project_transfer.history_model') as hm: jm.get_transfer_job.return_value = _job() hm.set_destination_artists.return_value = (None, [99]) result = logic.set_destination_artists(1, {'updates': [ {'release_id': 10, 'destination_artist_id': 777}, {'release_id': 99, 'destination_artist_id': 888}, ]}) assert result.status == 404 def test_set_destination_artists_400_when_updates_missing(): """Returns 400 when 'updates' is missing or empty.""" assert logic.set_destination_artists(1, {}).status == 400 assert logic.set_destination_artists(1, {'updates': []}).status == 400 def test_set_destination_artists_400_when_item_field_missing(): """Returns 400 when an update item is missing destination_artist_id.""" result = logic.set_destination_artists(1, {'updates': [ {'release_id': 10}, ]}) assert result.status == 400 def test_delete_soft_deletes_job_and_cascades_to_history(): """Soft-deletes the job and cascades to its product_transfer_history rows.""" with patch('project_manager.logic.project_transfer.job_model') as jm, \ patch('project_manager.logic.project_transfer.history_model') as hm: jm.get_transfer_job.return_value = _job(status='QUEUED') jm.soft_delete_transfer_job.return_value = True hm.soft_delete_for_job.return_value = 3 result = logic.delete_transfer_job(1, _IDENTITY_UUID) assert result.status == 204 jm.soft_delete_transfer_job.assert_called_once_with(1, _IDENTITY_UUID) hm.soft_delete_for_job.assert_called_once_with(1, _IDENTITY_UUID) def test_delete_returns_404_when_not_found(): """Returns 404 and does not touch the DB when job does not exist.""" with patch('project_manager.logic.project_transfer.job_model') as jm: jm.get_transfer_job.return_value = None result = logic.delete_transfer_job(999, _IDENTITY_UUID) assert result.status == 404 def test_delete_returns_400_when_not_queued(): """Returns 400 and skips deletion when job is not in QUEUED status.""" with patch('project_manager.logic.project_transfer.job_model') as jm: jm.get_transfer_job.return_value = _job(status='PROCESSING') result = logic.delete_transfer_job(1, _IDENTITY_UUID) assert result.status == 400 # --------------------------------------------------------------------------- # update_transfer_job # --------------------------------------------------------------------------- def _updated_job(**kwargs): return {**_job(), **kwargs} def test_update_sets_status_completed_and_auto_transfer_completed_on(): """COMPLETED status auto-populates transfer_completed_on when not supplied.""" with patch('project_manager.logic.project_transfer.job_model') as jm: jm.get_transfer_job.return_value = _job(status='PROCESSING') jm.update_transfer_job.return_value = _updated_job( status='COMPLETED', transfer_completed_on='2026-05-14T00:00:00') result = logic.update_transfer_job(1, {'status': 'COMPLETED'}) assert result.status == 200 assert result.message['status'] == 'COMPLETED' fields = jm.update_transfer_job.call_args[0][1] assert jm.ProjectTransferJob.transfer_completed_on in fields def test_update_sets_status_failed_with_failure_reason(): """FAILED status with failure_reason updates both columns.""" with patch('project_manager.logic.project_transfer.job_model') as jm: jm.get_transfer_job.return_value = _job(status='PROCESSING') jm.update_transfer_job.return_value = _updated_job( status='FAILED', failure_reason='timeout') result = logic.update_transfer_job( 1, {'status': 'FAILED', 'failure_reason': 'timeout'}) assert result.status == 200 def test_update_sets_status_processing_with_sfn_arn(): """PROCESSING + sfn_execution_arn sets both fields.""" arn = 'arn:aws:states:us-east-1:123:execution:my-sfn:abc' with patch('project_manager.logic.project_transfer.job_model') as jm: jm.get_transfer_job.return_value = _job(status='QUEUED') jm.update_transfer_job.return_value = _updated_job( status='PROCESSING', sfn_execution_arn=arn) result = logic.update_transfer_job( 1, {'status': 'PROCESSING', 'sfn_execution_arn': arn}) assert result.status == 200 fields = jm.update_transfer_job.call_args[0][1] assert fields[jm.ProjectTransferJob.sfn_execution_arn] == arn def test_update_accepts_explicit_transfer_completed_on(): """An explicit transfer_completed_on ISO string is parsed and stored.""" import datetime with patch('project_manager.logic.project_transfer.job_model') as jm: jm.get_transfer_job.return_value = _job(status='PROCESSING') jm.update_transfer_job.return_value = _updated_job(status='COMPLETED') logic.update_transfer_job( 1, {'status': 'COMPLETED', 'transfer_completed_on': '2026-05-14T12:00:00'}) fields = jm.update_transfer_job.call_args[0][1] assert fields[jm.ProjectTransferJob.transfer_completed_on] == datetime.datetime( 2026, 5, 14, 12, 0, 0) def test_update_parses_revenue_cutoff_date(): """revenue_cutoff_date string is converted to a date object.""" import datetime with patch('project_manager.logic.project_transfer.job_model') as jm: jm.get_transfer_job.return_value = _job(status='QUEUED') jm.update_transfer_job.return_value = _updated_job() logic.update_transfer_job(1, {'revenue_cutoff_date': '2026-06-01'}) fields = jm.update_transfer_job.call_args[0][1] assert fields[jm.ProjectTransferJob.revenue_cutoff_date] == datetime.date(2026, 6, 1) def test_update_returns_400_for_empty_body(): """A body with no recognised fields is rejected immediately.""" result = logic.update_transfer_job(1, {}) assert result.status == 400 def test_update_returns_400_for_unknown_fields_only(): """Unknown keys are stripped; if nothing remains the request is rejected.""" result = logic.update_transfer_job(1, {'foo': 'bar'}) assert result.status == 400 def test_update_returns_400_for_queued_status(): """Setting status back to QUEUED via this endpoint is not allowed.""" result = logic.update_transfer_job(1, {'status': 'QUEUED'}) assert result.status == 400 def test_update_returns_400_for_failed_without_failure_reason(): """FAILED status without failure_reason returns 400.""" result = logic.update_transfer_job(1, {'status': 'FAILED'}) assert result.status == 400 def test_update_returns_400_for_invalid_revenue_cutoff_date(): """A non-ISO revenue_cutoff_date returns 400.""" result = logic.update_transfer_job(1, {'revenue_cutoff_date': 'not-a-date'}) assert result.status == 400 def test_update_returns_400_for_invalid_transfer_completed_on(): """A non-ISO transfer_completed_on returns 400.""" result = logic.update_transfer_job( 1, {'transfer_completed_on': 'not-a-datetime'}) assert result.status == 400 def test_update_returns_400_for_sfn_arn_too_long(): """sfn_execution_arn longer than 2048 chars returns 400.""" result = logic.update_transfer_job( 1, {'sfn_execution_arn': 'x' * 2049}) assert result.status == 400 def test_update_returns_404_when_job_not_found(): """Returns 404 when the job does not exist.""" with patch('project_manager.logic.project_transfer.job_model') as jm: jm.get_transfer_job.return_value = None result = logic.update_transfer_job(1, {'status': 'PROCESSING'}) assert result.status == 404 def test_update_returns_400_when_job_already_completed(): """Cannot update a COMPLETED (terminal) job.""" with patch('project_manager.logic.project_transfer.job_model') as jm: jm.get_transfer_job.return_value = _job(status='COMPLETED') result = logic.update_transfer_job(1, {'status': 'FAILED', 'failure_reason': 'late'}) assert result.status == 400 def test_update_returns_400_when_job_already_failed(): """Cannot update a FAILED (terminal) job.""" with patch('project_manager.logic.project_transfer.job_model') as jm: jm.get_transfer_job.return_value = _job(status='FAILED') result = logic.update_transfer_job(1, {'status': 'COMPLETED'}) assert result.status == 400 def test_update_sets_destination_artist_id(): """destination_artist_id integer is forwarded to the model.""" with patch('project_manager.logic.project_transfer.job_model') as jm: jm.get_transfer_job.return_value = _job(status='PROCESSING') jm.update_transfer_job.return_value = _updated_job() result = logic.update_transfer_job(1, {'destination_artist_id': 42}) assert result.status == 200 fields = jm.update_transfer_job.call_args[0][1] assert jm.ProjectTransferJob.destination_artist_id in fields assert fields[jm.ProjectTransferJob.destination_artist_id] == 42 def test_update_returns_400_for_non_int_destination_artist_id(): """Non-integer destination_artist_id returns 400.""" result = logic.update_transfer_job(1, {'destination_artist_id': 'not-an-int'}) assert result.status == 400 # --------------------------------------------------------------------------- # execute_content_transfer # --------------------------------------------------------------------------- def _exec_job(): return _job(destination_vendor_id=200, destination_artist_id=77, destination_subaccount_id=5) def _exec_products(): return [ { 'product_transfer_history_id': 1, 'project_transfer_job_id': 1, 'release_id': 10, 'source_artist_id': 50, 'destination_artist_id': 77, 'release_name': 'Album One', 'upc': '123456789012', 'display_upc': '1-23456-78901-2', }, { 'product_transfer_history_id': 2, 'project_transfer_job_id': 1, 'release_id': 11, 'source_artist_id': 51, 'destination_artist_id': 78, 'release_name': None, 'upc': None, 'display_upc': None, }, ] def test_execute_content_transfer_happy_path(): """Returns 200 with update counts; model called with all job/product params.""" model_result = {'project_updated': 1, 'releases_updated': 2, 'video_rows_updated': 0} with patch('project_manager.logic.project_transfer.job_model') as jm, \ patch('project_manager.logic.project_transfer.history_model') as hm: jm.get_transfer_job.return_value = _exec_job() hm.get_products_for_job.return_value = _exec_products() jm.execute_content_transfer.return_value = model_result result = logic.execute_content_transfer(1, 'test-identity') assert result.status == 200 assert result.message == model_result jm.execute_content_transfer.assert_called_once_with( job_id=1, project_id=10, destination_vendor_id=200, destination_subaccount_id=5, destination_artist_id=77, products=_exec_products(), ) jm.set_executed_by_identity_id.assert_called_once_with(1, 'test-identity') def test_execute_content_transfer_404_when_job_not_found(): """Returns 404 when the job does not exist.""" with patch('project_manager.logic.project_transfer.job_model') as jm: jm.get_transfer_job.return_value = None result = logic.execute_content_transfer(999, 'test-identity') assert result.status == 404 def test_execute_content_transfer_400_when_no_products(): """Returns 400 when the job has no product snapshot rows.""" with patch('project_manager.logic.project_transfer.job_model') as jm, \ patch('project_manager.logic.project_transfer.history_model') as hm: jm.get_transfer_job.return_value = _exec_job() hm.get_products_for_job.return_value = [] result = logic.execute_content_transfer(1, 'test-identity') assert result.status == 400 def test_execute_content_transfer_422_when_destination_artist_missing(): """Returns 422 and does not call the model when any destination_artist_id is None.""" products = _exec_products() products[1]['destination_artist_id'] = None # release 11 not resolved yet with patch('project_manager.logic.project_transfer.job_model') as jm, \ patch('project_manager.logic.project_transfer.history_model') as hm: jm.get_transfer_job.return_value = _exec_job() hm.get_products_for_job.return_value = products result = logic.execute_content_transfer(1, 'test-identity') assert result.status == 422 jm.execute_content_transfer.assert_not_called() def test_execute_content_transfer_propagates_model_error(): """A Response returned by the model (e.g. DB error) is passed through.""" db_error = response.create_fatal_response() with patch('project_manager.logic.project_transfer.job_model') as jm, \ patch('project_manager.logic.project_transfer.history_model') as hm: jm.get_transfer_job.return_value = _exec_job() hm.get_products_for_job.return_value = _exec_products() jm.execute_content_transfer.return_value = db_error result = logic.execute_content_transfer(1, 'test-identity') assert result is db_error def test_execute_content_transfer_derives_artist_from_products_when_job_artist_is_none(): """When job.destination_artist_id is None, falls back to products[0].destination_artist_id.""" job_no_artist = _job(destination_vendor_id=200, destination_artist_id=None, destination_subaccount_id=5) model_result = {'project_updated': 1, 'releases_updated': 2, 'video_rows_updated': 0} with patch('project_manager.logic.project_transfer.job_model') as jm, \ patch('project_manager.logic.project_transfer.history_model') as hm: jm.get_transfer_job.return_value = job_no_artist hm.get_products_for_job.return_value = _exec_products() jm.execute_content_transfer.return_value = model_result result = logic.execute_content_transfer(1, 'test-identity') assert result.status == 200 _, kwargs = jm.execute_content_transfer.call_args assert kwargs['destination_artist_id'] == 77 # products[0]['destination_artist_id'] # --------------------------------------------------------------------------- # get_transfer_job_attachments # --------------------------------------------------------------------------- def test_get_attachments_returns_upcs_and_isrcs(): """Returns dict with both lists for an existing job.""" with patch('project_manager.logic.project_transfer.job_model') as jm, \ patch('project_manager.logic.project_transfer.history_model') as hm: jm.get_transfer_job.return_value = _job() hm.get_upcs_for_job.return_value = ['5054526566132', '20286137634'] hm.get_isrcs_for_job.return_value = ['GBKPL1356311', 'USRC12345678'] result = logic.get_transfer_job_attachments(1) assert result.status == 200 assert result.message == { 'upcs': ['5054526566132', '20286137634'], 'isrcs': ['GBKPL1356311', 'USRC12345678'], } hm.get_upcs_for_job.assert_called_once_with(1) hm.get_isrcs_for_job.assert_called_once_with(1) def test_get_attachments_returns_empty_lists_for_job_with_no_releases(): """Returns 200 with empty lists when the job has no resolvable attachments.""" with patch('project_manager.logic.project_transfer.job_model') as jm, \ patch('project_manager.logic.project_transfer.history_model') as hm: jm.get_transfer_job.return_value = _job() hm.get_upcs_for_job.return_value = [] hm.get_isrcs_for_job.return_value = [] result = logic.get_transfer_job_attachments(1) assert result.status == 200 assert result.message == {'upcs': [], 'isrcs': []} def test_get_attachments_returns_404_when_job_missing(): """Returns 404 without querying releases/track when the job does not exist.""" with patch('project_manager.logic.project_transfer.job_model') as jm, \ patch('project_manager.logic.project_transfer.history_model') as hm: jm.get_transfer_job.return_value = None result = logic.get_transfer_job_attachments(999) assert result.status == 404 hm.get_upcs_for_job.assert_not_called() hm.get_isrcs_for_job.assert_not_called() def test_get_attachments_short_circuits_on_upc_query_error(): """If the UPC query returns a Response error, isrc query is not called.""" with patch('project_manager.logic.project_transfer.job_model') as jm, \ patch('project_manager.logic.project_transfer.history_model') as hm: jm.get_transfer_job.return_value = _job() err = response.create_error_response( code='db_error', message='boom', status=500) hm.get_upcs_for_job.return_value = err result = logic.get_transfer_job_attachments(1) assert result.status == 500 hm.get_isrcs_for_job.assert_not_called()