import pytest from pytest_mock import MockerFixture from src.index import handler _JOB = {"destination_vendor_id": 99, "originating_artist_id": 101} def _product( release_id: int, source_artist_id: int | None, destination_artist_id: int | None, source_video_artist_id: int | None = None, destination_video_artist_id: int | None = None, ) -> dict[str, object]: return { "release_id": release_id, "source_artist_id": source_artist_id, "destination_artist_id": destination_artist_id, "source_video_artist_id": source_video_artist_id, "destination_video_artist_id": destination_video_artist_id, } def test_handler_ensures_artists_and_updates_products(mocker: MockerFixture) -> None: mock_get_job = mocker.patch("src.index.get_transfer_job") mock_get_products = mocker.patch("src.index.get_transfer_job_products") mock_bulk_ensure = mocker.patch("src.index.bulk_ensure_artists") mock_set_destination_artists = mocker.patch("src.index.set_destination_artists") mock_update_job = mocker.patch("src.index.update_transfer_job") mock_get_job.return_value = _JOB mock_get_products.return_value = [ _product(10, source_artist_id=101, destination_artist_id=None), _product(11, source_artist_id=102, destination_artist_id=None), _product(12, source_artist_id=101, destination_artist_id=None), ] mock_bulk_ensure.return_value = {"101": 201, "102": 202} mock_set_destination_artists.return_value = {"updated_count": 3} result = handler({"job_id": 42}, object()) assert result == { "job_id": 42, "ensure_artists_result": { "requested_source_artist_ids": [101, 102], "artist_mapping": {"101": 201, "102": 202}, "updated_count": 3, "destination_artist_id": 201, "skipped": False, }, } mock_get_job.assert_called_once_with(42) mock_get_products.assert_called_once_with(42, 99) mock_bulk_ensure.assert_called_once_with([101, 102], 99) mock_set_destination_artists.assert_called_once_with( 42, [ {"release_id": 10, "destination_artist_id": 201, "destination_video_artist_id": None}, {"release_id": 11, "destination_artist_id": 202, "destination_video_artist_id": None}, {"release_id": 12, "destination_artist_id": 201, "destination_video_artist_id": None}, ], ) mock_update_job.assert_called_once_with(42, 201) def test_handler_ensures_video_artists_for_video_products(mocker: MockerFixture) -> None: """source_video_artist_id is included in bulk_ensure and destination_video_artist_id is set.""" mock_get_job = mocker.patch("src.index.get_transfer_job") mock_get_products = mocker.patch("src.index.get_transfer_job_products") mock_bulk_ensure = mocker.patch("src.index.bulk_ensure_artists") mock_set_destination_artists = mocker.patch("src.index.set_destination_artists") mocker.patch("src.index.update_transfer_job") mock_get_job.return_value = _JOB mock_get_products.return_value = [ _product(10, source_artist_id=101, destination_artist_id=None, source_video_artist_id=500), _product(11, source_artist_id=102, destination_artist_id=None), ] mock_bulk_ensure.return_value = {"101": 201, "102": 202, "500": 600} mock_set_destination_artists.return_value = {"updated_count": 2} result = handler({"job_id": 42}, object()) # 101 and originating_artist_id (101) plus 102 and video artist 500 mock_bulk_ensure.assert_called_once_with([101, 102, 500], 99) mock_set_destination_artists.assert_called_once_with( 42, [ {"release_id": 10, "destination_artist_id": 201, "destination_video_artist_id": 600}, {"release_id": 11, "destination_artist_id": 202, "destination_video_artist_id": None}, ], ) assert result["ensure_artists_result"]["destination_artist_id"] == 201 def test_handler_skips_when_all_artists_already_resolved(mocker: MockerFixture) -> None: mock_get_job = mocker.patch("src.index.get_transfer_job") mock_get_products = mocker.patch("src.index.get_transfer_job_products") mock_bulk_ensure = mocker.patch("src.index.bulk_ensure_artists") mock_set_destination_artists = mocker.patch("src.index.set_destination_artists") mock_get_job.return_value = _JOB mock_get_products.return_value = [ _product(10, source_artist_id=101, destination_artist_id=201), ] result = handler({"job_id": 42}, object()) assert result == { "job_id": 42, "ensure_artists_result": { "requested_source_artist_ids": [], "artist_mapping": {}, "updated_count": 0, "skipped": True, }, } mock_get_products.assert_called_once_with(42, 99) mock_bulk_ensure.assert_not_called() mock_set_destination_artists.assert_not_called() def test_handler_not_skipped_when_video_artist_unresolved(mocker: MockerFixture) -> None: """A product with destination_artist_id set but destination_video_artist_id missing is processed.""" mock_get_job = mocker.patch("src.index.get_transfer_job") mock_get_products = mocker.patch("src.index.get_transfer_job_products") mock_bulk_ensure = mocker.patch("src.index.bulk_ensure_artists") mock_set_destination_artists = mocker.patch("src.index.set_destination_artists") mocker.patch("src.index.update_transfer_job") mock_get_job.return_value = _JOB mock_get_products.return_value = [ _product( 10, source_artist_id=101, destination_artist_id=201, source_video_artist_id=500, destination_video_artist_id=None, ), ] mock_bulk_ensure.return_value = {"101": 201, "500": 600} mock_set_destination_artists.return_value = {"updated_count": 1} handler({"job_id": 42}, object()) mock_bulk_ensure.assert_called_once() mock_set_destination_artists.assert_called_once_with( 42, [{"release_id": 10, "destination_artist_id": 201, "destination_video_artist_id": 600}], ) def test_handler_includes_originating_artist_id_in_bulk_ensure_when_not_in_products( mocker: MockerFixture, ) -> None: """originating_artist_id is sent to bulk-ensure even if no product has it as source.""" mock_get_job = mocker.patch("src.index.get_transfer_job") mock_get_products = mocker.patch("src.index.get_transfer_job_products") mock_bulk_ensure = mocker.patch("src.index.bulk_ensure_artists") mock_set_destination_artists = mocker.patch("src.index.set_destination_artists") mock_update_job = mocker.patch("src.index.update_transfer_job") mock_get_job.return_value = {"destination_vendor_id": 99, "originating_artist_id": 101} mock_get_products.return_value = [ _product(10, source_artist_id=102, destination_artist_id=None), ] mock_bulk_ensure.return_value = {"101": 201, "102": 202} mock_set_destination_artists.return_value = {"updated_count": 1} result = handler({"job_id": 42}, object()) mock_bulk_ensure.assert_called_once_with([101, 102], 99) mock_update_job.assert_called_once_with(42, 201) assert result["ensure_artists_result"]["destination_artist_id"] == 201 def test_handler_raises_on_missing_job_id() -> None: with pytest.raises(KeyError): handler({}, object()) def test_handler_raises_on_invalid_job_id() -> None: with pytest.raises(ValueError, match="must be an integer"): handler({"job_id": "not-an-int"}, object()) def test_handler_raises_when_originating_artist_id_is_none(mocker: MockerFixture) -> None: mock_get_job = mocker.patch("src.index.get_transfer_job") mock_get_job.return_value = {"destination_vendor_id": 99, "originating_artist_id": None} with pytest.raises(RuntimeError, match="originating_artist_id is not set"): handler({"job_id": 42}, object()) def test_handler_raises_on_missing_source_artist_id(mocker: MockerFixture) -> None: mock_get_job = mocker.patch("src.index.get_transfer_job") mock_get_products = mocker.patch("src.index.get_transfer_job_products") mock_get_job.return_value = _JOB mock_get_products.return_value = [ _product(10, source_artist_id=None, destination_artist_id=None), ] with pytest.raises(RuntimeError, match="missing source_artist_id"): handler({"job_id": 42}, object())