from unittest.mock import MagicMock import pytest from pytest_mock import MockerFixture from src.errors import PermanentError, TransientError from src.index import handler _EVENT = {"job_id": 42} _JOB = {"originating_vendor_id": 10, "destination_vendor_id": 20} _ATTACHMENTS = {"upcs": ["00602458823264"], "isrcs": ["USRC17607839"]} _REMOVAL_RESULT = {"updated_terms": [], "total_removed": 1} _STAGED_TERMS = [{"project_transfer_term_id": 1, "contract_id": 555, "term_type": "product"}] _ADD_RESULT = [ { "project_transfer_term_id": 1, "contract_id": 555, "term_type": "product", "destination_contract_term_id": 99001, "response": {"added": 1}, } ] def _patch_all(mocker: MockerFixture) -> tuple[MagicMock, MagicMock, MagicMock, MagicMock, MagicMock]: mock_job = mocker.patch("src.index.get_transfer_job", return_value=_JOB) mock_resolve = mocker.patch("src.index.resolve_attachments_for_job", return_value=_ATTACHMENTS) mock_remove = mocker.patch("src.index.remove_upcs_from_originating_account", return_value=_REMOVAL_RESULT) mock_fetch = mocker.patch("src.index.fetch_staged_terms_for_job", return_value=_STAGED_TERMS) mock_add = mocker.patch("src.index.add_to_destination_for_each_term", return_value=_ADD_RESULT) return mock_job, mock_resolve, mock_remove, mock_fetch, mock_add def test_handler_full_integration(mocker: MockerFixture) -> None: mock_job, mock_resolve, mock_remove, mock_fetch, mock_add = _patch_all(mocker) result = handler(_EVENT, object()) assert result["status"] == "complete" assert result["job_id"] == 42 assert result["originating_vendor_id"] == 10 assert result["destination_vendor_id"] == 20 assert result["attachments"] == _ATTACHMENTS assert result["attachment_removal"] == _REMOVAL_RESULT assert result["attachment_addition"] == _ADD_RESULT mock_job.assert_called_once_with(42) mock_resolve.assert_called_once_with(42) mock_remove.assert_called_once_with(account_id=10, upcs=_ATTACHMENTS["upcs"], isrcs=_ATTACHMENTS["isrcs"]) mock_fetch.assert_called_once_with(42) mock_add.assert_called_once_with( destination_vendor_id=20, staged_terms=_STAGED_TERMS, ) def test_handler_no_staged_terms_still_completes(mocker: MockerFixture) -> None: mocker.patch("src.index.get_transfer_job", return_value=_JOB) mocker.patch("src.index.resolve_attachments_for_job", return_value=_ATTACHMENTS) mocker.patch("src.index.remove_upcs_from_originating_account", return_value=_REMOVAL_RESULT) mocker.patch("src.index.fetch_staged_terms_for_job", return_value=[]) mock_add = mocker.patch("src.index.add_to_destination_for_each_term", return_value=[]) result = handler(_EVENT, object()) assert result["status"] == "complete" assert result["attachment_addition"] == [] mock_add.assert_called_once_with(destination_vendor_id=20, staged_terms=[]) def test_handler_missing_job_id_raises_permanent_error() -> None: with pytest.raises(PermanentError, match="job_id"): handler({"other": 1}, object()) def test_handler_wrong_type_raises_permanent_error() -> None: with pytest.raises(PermanentError, match="must be an integer"): handler({"job_id": "not-int"}, object()) def test_handler_empty_event_raises_permanent_error() -> None: with pytest.raises(PermanentError, match="empty"): handler({}, object()) def test_handler_propagates_transient_error(mocker: MockerFixture) -> None: mocker.patch("src.index.get_transfer_job", side_effect=TransientError("upstream down")) with pytest.raises(TransientError): handler(_EVENT, object()) def test_handler_propagates_permanent_error(mocker: MockerFixture) -> None: mocker.patch("src.index.get_transfer_job", side_effect=PermanentError("job not found")) with pytest.raises(PermanentError): handler(_EVENT, object()) def test_handler_completes_when_all_terms_skipped(mocker: MockerFixture) -> None: mocker.patch("src.index.get_transfer_job", return_value=_JOB) mocker.patch("src.index.resolve_attachments_for_job", return_value=_ATTACHMENTS) mocker.patch("src.index.remove_upcs_from_originating_account", return_value=_REMOVAL_RESULT) mocker.patch("src.index.fetch_staged_terms_for_job", return_value=_STAGED_TERMS) skipped_entry = { "project_transfer_term_id": 1, "contract_id": 555, "term_type": "artist", "skipped": "unsupported_term_type", } mocker.patch("src.index.add_to_destination_for_each_term", return_value=[skipped_entry]) result = handler(_EVENT, object()) assert result["status"] == "complete" assert result["attachment_addition"] == [skipped_entry]