"""Tests for Manager.""" from typing import Any from unittest.mock import MagicMock, patch import pytest from pytest_mock import MockerFixture from gridgen.exceptions import ( ReleaseLookupError, ReleaseNotFound, TrackLookupError, TrackNotFound, ValidateInternalError, ValidateNotFoundError, ) from gridgen.logic import manager from gridgen.models.datasource import delphi, orchard from gridgen.models.metadata import ( ArtistMeta, GridMeta, Release, ReleaseIdMeta, ReleaseMeta, TrackIdMeta, TrackMeta, ) def gen_track_meta(ix: str) -> TrackMeta: """Create test track""" return TrackMeta( 314234, "upc_{}".format(ix), "isrc_{}".format(ix), "track_name_{}".format(ix), "track_supplemental_info_{}".format(ix), "p_line_{}".format(ix), "track_type_{}".format(ix), 3143, set(), set(), ) def gen_release_meta(ix: str) -> ReleaseMeta: """Create test release""" return ReleaseMeta( hash("release_id_{}".format(ix)), "upc_{}".format(ix), "grid_{}".format(ix), "product_number_{}".format(ix), "release_name_{}".format(ix), "release_supplemental_info_{}".format(ix), "configuration_{}".format(ix), "repertoire_owner_{}".format(ix), "display_artist_name_{}".format(ix), "gras_status_{}".format(ix), "format_{}".format(ix), ) def generate_release_identifier(release_id: int) -> ReleaseIdMeta: """Create test release ID metadata.""" return ReleaseIdMeta(release_id, str(release_id)) def generate_release( id_meta: ReleaseIdMeta, use_prod_num: bool = True, is_complete: bool = True, is_physical: bool = False, ) -> ReleaseMeta: """Create test release metadata.""" test_grid = "grid#_{}".format(id_meta.upc) if use_prod_num else None test_prod = "prod#_{}".format(id_meta.upc) if use_prod_num else None test_status = "complete" if is_complete else "incomplete" test_format = "physical" if is_physical else "digital" return ReleaseMeta( id_meta.release_id, id_meta.upc, test_grid, test_prod, "name_{}".format(id_meta.upc), "info_{}".format(id_meta.upc), "config_{}".format(id_meta.upc), "owner_{}".format(id_meta.upc), "artist_{}".format(id_meta.upc), test_status, test_format, ) def generate_track(id_meta: TrackIdMeta, use_prod_num: bool = True) -> TrackMeta: """Create test track metadata.""" text = "{}_{}".format(id_meta.upc, id_meta.isrc) test_grid_meta = ( {GridMeta("grid#_{}".format(text), "prod#_{}".format(text))} if use_prod_num else set() ) # test duration cycles between 1 of 3 possible test values duration_values = { "ISRC00": 120, "ISRC01": 125, "ISRC02": 130, "ISRC03": 120, "ISRC04": 125, "ISRC05": 130, "ISRC06": 120, "ISRC07": 125, "ISRC08": 130, "ISRC09": 120, "ISRC10": 125, "ISRC11": 130, "ISRC12": 120, "ISRC13": 125, "ISRC14": 130, } test_duration = duration_values[id_meta.isrc] return TrackMeta( id_meta.track_id, id_meta.upc, id_meta.isrc, "name_{}".format(text), "info_{}".format(text), "pline_{}".format(text), "type_{}".format(text), test_duration, {ArtistMeta("artist_{}".format(text), "role_{}".format(text))}, test_grid_meta, ) # grid meta tuple with GRid and product number produced by mock generator GRID_META_TEST_MOCK = GridMeta("A10426HABCDE12345Z", "G04ABCDE12345Z") def generate_orchard_release(id_meta: ReleaseIdMeta) -> ReleaseMeta | None: """Generate a test Orchard release.""" test_releases = { 0: True, 1: False, 2: True, 3: False, 4: True, 5: False, 6: True, 7: False, 8: True, 9: False, } if id_meta.release_id in test_releases: return generate_release( id_meta, test_releases[id_meta.release_id], test_releases[id_meta.release_id], ) return None def generate_gras_release(id_meta: ReleaseIdMeta) -> ReleaseMeta: """Generate a test GRAS release.""" test_releases = {0: True, 1: False, 2: False, 3: True, 4: False} if id_meta.release_id in test_releases: return generate_release( id_meta, test_releases[id_meta.release_id], test_releases[id_meta.release_id], ) elif id_meta.release_id is None or id_meta.release_id < 100: raise ReleaseNotFound("Error finding GRAS release") else: raise Exception("Error retrieving GRAS release") def generate_orchard_track(id_meta: TrackIdMeta) -> TrackMeta: """Generate a test Orchard track.""" test_tracks = { "ISRC00": True, "ISRC01": False, "ISRC02": True, "ISRC03": False, "ISRC04": True, "ISRC05": False, "ISRC06": True, "ISRC07": False, "ISRC08": True, "ISRC09": False, "ISRC10": True, "ISRC11": False, "ISRC12": True, "ISRC13": False, "ISRC14": True, } if id_meta.isrc in test_tracks: return generate_track(id_meta, test_tracks[id_meta.isrc]) else: raise Exception("Error finding Orchard track") def generate_gras_track(id_meta: TrackIdMeta) -> TrackMeta: """Generate a test GRAS track.""" test_tracks = { "ISRC00": True, "ISRC01": False, "ISRC02": False, "ISRC03": True, "ISRC04": False, "ISRC05": False, "ISRC06": True, "ISRC07": False, "ISRC08": False, "ISRC09": True, } if id_meta.isrc in test_tracks: return generate_track(id_meta, test_tracks[id_meta.isrc]) elif id_meta.track_id is None or id_meta.track_id < 20100: raise TrackNotFound("Error finding GRAS track") else: raise Exception("Error retrieving GRAS track") @patch( "gridgen.logic.manager.build_release", return_value=Release(generate_release(ReleaseIdMeta(123, "123"), True)), ) @patch( "gridgen.models.datasource.orchard.lookup_release_identifiers", return_value=ReleaseIdMeta(123, "123"), ) @patch("gridgen.models.datasource.orchard.lookup_track_list") def test_synchronize_for_empty_tracks_digital_failure( lookup_track_list: MagicMock, _mock_identifiers: MagicMock, _mock_release: MagicMock ) -> None: """Test synchronizing UPC.""" error_message = "Error locating track list for 123" lookup_track_list.side_effect = Exception(error_message) with pytest.raises(Exception) as exc: manager.synchronize(123) assert str(exc.value) == error_message def mock_data_source_from_test_diff() -> MagicMock: """Create a mock data source object using test_diff module.""" data_source = MagicMock() data_source.lookup_release.side_effect = lambda meta_id: gen_release_meta( meta_id.upc ) data_source.lookup_track.side_effect = lambda meta_id: gen_track_meta(meta_id.isrc) return data_source @pytest.mark.parametrize( ( "lookup_release_side_effect", "lookup_track_side_effect", "track_list", "expected_result", ), [ pytest.param( ReleaseNotFound("release not found"), TrackNotFound("track not found"), [], Release(None), id="nothing found", ), ], ) def test_build_release_errors( lookup_release_side_effect: Any, lookup_track_side_effect: Any, track_list: list[TrackIdMeta], expected_result: Release, ) -> None: """Test build_release with a data source that never finds anything.""" data_source = MagicMock() data_source.lookup_release.side_effect = lookup_release_side_effect data_source.lookup_track.side_effect = lookup_track_side_effect new_release = manager.build_release( ReleaseIdMeta(None, "my_upc"), data_source, track_list ) assert new_release.metadata == expected_result.metadata assert new_release.tracks == expected_result.tracks def test_build_release_lookup_release_error() -> None: """Test build_release raises ReleaseLookupError on unexpected release lookup failure.""" data_source = MagicMock() data_source.lookup_release.side_effect = Exception("query error") with pytest.raises(ReleaseLookupError) as exc: manager.build_release(ReleaseIdMeta(None, "my_upc"), data_source, []) assert exc.value.description == "Error retrieving release info for my_upc." def test_build_release_lookup_track_error() -> None: """Test build_release raises TrackLookupError on unexpected track lookup failure.""" data_source = MagicMock() data_source.lookup_release.side_effect = lambda _: gen_release_meta("my_upc") data_source.lookup_track.side_effect = Exception("query error") with pytest.raises(TrackLookupError) as exc: manager.build_release( ReleaseIdMeta(None, "my_upc"), data_source, [TrackIdMeta(1, "ISRC01", "01234567890")], ) assert exc.value.description == "Error retrieving track info for ISRC01." def test_build_release_with_no_tracks() -> None: """Test build_release, no ISRCs specified for tracks.""" data_source = mock_data_source_from_test_diff() expected = Release(gen_release_meta("my_upc")) new_release = manager.build_release(ReleaseIdMeta(None, "my_upc"), data_source) assert data_source.lookup_release.called assert data_source.lookup_track.not_called assert expected.tracks == new_release.tracks == {} assert expected.metadata == new_release.metadata def test_build_release_with_tracks() -> None: """Test build_release with ISRCs specified.""" data_source = mock_data_source_from_test_diff() track_list = [ TrackIdMeta(0, "ISRC01", "01234567890"), TrackIdMeta(1, "ISRC02", "01234567890"), ] expected = Release( gen_release_meta("my_upc"), {gen_track_meta("ISRC01"), gen_track_meta("ISRC02")}, ) new_release = manager.build_release( ReleaseIdMeta(None, "my_upc"), data_source, track_list ) assert data_source.lookup_release.called assert data_source.lookup_track.call_count == len(expected.tracks) assert expected.tracks == new_release.tracks assert expected.metadata == new_release.metadata @pytest.mark.parametrize( ("release_id_meta", "track_list", "orchard_release_lookup"), [ pytest.param( ReleaseIdMeta(0, "00"), [TrackIdMeta(20000, "ISRC00", "0"), TrackIdMeta(20006, "ISRC06", "0")], generate_orchard_release, id="full match", ), pytest.param( ReleaseIdMeta(3, "03"), [TrackIdMeta(20000, "ISRC00", "3"), TrackIdMeta(20006, "ISRC06", "3")], generate_orchard_release, id="match without release product numbers", ), pytest.param( ReleaseIdMeta(0, "00"), [TrackIdMeta(20000, "ISRC00", "0"), TrackIdMeta(20003, "ISRC03", "0")], generate_orchard_release, id="match without track product numbers", ), pytest.param( ReleaseIdMeta(0, "00"), [TrackIdMeta(20000, "ISRC00", "00"), TrackIdMeta(20006, "ISRC06", "00")], lambda _: generate_release(ReleaseIdMeta(0, "00"), True, True, True), id="physical release", ), ], ) def test_synchronize_no_sync( mocker: MockerFixture, release_id_meta: ReleaseIdMeta, track_list: list[TrackIdMeta], orchard_release_lookup: Any, ) -> None: """Test synchronize cases where no delivery message should be sent.""" mocker.patch.object(orchard, "save_track", return_value=None) mocker.patch.object(orchard, "save_release", return_value=None) mocker.patch.object( orchard, "lookup_release_identifiers", return_value=release_id_meta ) mocker.patch.object(orchard, "lookup_track_list", return_value=track_list) mocker.patch.object(orchard, "lookup_release", side_effect=orchard_release_lookup) mocker.patch.object(orchard, "lookup_track", side_effect=generate_orchard_track) mocker.patch.object(delphi, "lookup_release", side_effect=generate_gras_release) mocker.patch.object(delphi, "lookup_track", side_effect=generate_gras_track) assert release_id_meta.release_id is not None assert manager.synchronize(release_id_meta.release_id) == "" @pytest.mark.parametrize( ( "release_id_meta", "track_list", "orchard_release_lookup", "expected_save_release_call", "expected_save_track_count", ), [ pytest.param( ReleaseIdMeta(6, "06"), [TrackIdMeta(20000, "ISRC00", "6"), TrackIdMeta(20006, "ISRC06", "6")], generate_orchard_release, (ReleaseIdMeta(6, "06"), GridMeta("grid#_06", "prod#_06")), 2, id="no match upc with product numbers", ), pytest.param( ReleaseIdMeta(5, "05"), [TrackIdMeta(20000, "ISRC00", "5"), TrackIdMeta(20006, "ISRC06", "5")], generate_orchard_release, None, 2, id="no match upc without product numbers", ), pytest.param( ReleaseIdMeta(0, "00"), [TrackIdMeta(20000, "ISRC00", "0"), TrackIdMeta(20010, "ISRC10", "0")], generate_orchard_release, (ReleaseIdMeta(0, "00"), GridMeta("grid#_00", "prod#_00")), 2, id="no match track with product numbers", ), pytest.param( ReleaseIdMeta(0, "00"), [TrackIdMeta(20000, "ISRC00", "0"), TrackIdMeta(20011, "ISRC11", "0")], generate_orchard_release, None, None, id="no match track without product numbers", ), pytest.param( ReleaseIdMeta(5, "05"), [TrackIdMeta(20000, "ISRC00", "05"), TrackIdMeta(20006, "ISRC06", "05")], lambda _: generate_release(ReleaseIdMeta(5, "05"), False, False, True), (ReleaseIdMeta(5, "05"), GridMeta(None, "0")), 0, id="physical release new product number", ), ], ) def test_synchronize_sends_message( mocker: MockerFixture, release_id_meta: ReleaseIdMeta, track_list: list[TrackIdMeta], orchard_release_lookup: Any, expected_save_release_call: tuple[Any, ...] | None, expected_save_track_count: int | None, ) -> None: """Test synchronize cases where a delivery message should be sent.""" mock_save_track = mocker.patch.object(orchard, "save_track", return_value=None) mock_save_release = mocker.patch.object(orchard, "save_release", return_value=None) mocker.patch.object( orchard, "lookup_release_identifiers", return_value=release_id_meta ) mock_track_list = mocker.patch.object( orchard, "lookup_track_list", return_value=track_list ) mocker.patch.object(orchard, "lookup_release", side_effect=orchard_release_lookup) mocker.patch.object(orchard, "lookup_track", side_effect=generate_orchard_track) mocker.patch.object(delphi, "lookup_release", side_effect=generate_gras_release) mocker.patch.object(delphi, "lookup_track", side_effect=generate_gras_track) assert release_id_meta.release_id is not None assert manager.synchronize(release_id_meta.release_id) == release_id_meta.upc mock_track_list.assert_called_with(release_id_meta) if expected_save_release_call is not None: mock_save_release.assert_called_with(*expected_save_release_call) if expected_save_track_count is not None: assert mock_save_track.call_count == expected_save_track_count @patch("gridgen.logic.processor.synch_track", return_value=None) @patch("gridgen.logic.processor.synch_release", return_value=None) @patch( "gridgen.models.datasource.orchard.lookup_release_identifiers", return_value=ReleaseIdMeta(50, "50"), ) @patch( "gridgen.models.datasource.orchard.lookup_track_list", return_value=[ TrackIdMeta(20000, "ISRC00", "50"), TrackIdMeta(20006, "ISRC06", "50"), ], ) @patch( "gridgen.models.datasource.orchard.lookup_release", side_effect=generate_orchard_release, ) @patch( "gridgen.models.datasource.orchard.lookup_track", side_effect=generate_orchard_track ) @patch( "gridgen.models.datasource.delphi.lookup_release", side_effect=generate_gras_release ) @patch("gridgen.models.datasource.delphi.lookup_track", side_effect=generate_gras_track) def test_synchronize_with_no_release_metadata_gras_flag_enabled( _mock_gras_track: MagicMock, _mock_gras_release: MagicMock, _mock_orch_track: MagicMock, _mock_orch_release: MagicMock, mock_track_list: MagicMock, _mock_identifiers: MagicMock, mock_synch_release: MagicMock, mock_synch_track: MagicMock, ) -> None: """Test synchronize with missing release metadata.""" with pytest.raises(ReleaseNotFound): manager.synchronize(50) mock_track_list.assert_called_with(ReleaseIdMeta(50, "50")) assert not mock_synch_release.called assert not mock_synch_track.called @pytest.mark.parametrize( ( "release_id_meta", "track_list", "orchard_release_lookup", "validate_arg", "expected_result", "expected_exception", ), [ pytest.param( ReleaseIdMeta(0, "00"), [TrackIdMeta(20000, "ISRC00", "00"), TrackIdMeta(20011, "ISRC11", "00")], generate_orchard_release, 0, { "system_errors": [], "release": { "upc": "00", "product_no": "prod#_00", "grid": "grid#_00", "errors": {"metadata": [], "general": []}, }, "tracks": { 20000: { "isrc": "ISRC00", "product_no": "prod#_00_ISRC00", "grid": "grid#_00_ISRC00", "errors": {"metadata": [], "general": []}, } }, }, None, id="happy path", ), pytest.param( ReleaseIdMeta(0, "00"), [], generate_orchard_release, 0, { "system_errors": [ "Error locating track list from art_relations for 00" ], "release": {}, "tracks": {}, }, ValidateNotFoundError, id="no track list", ), pytest.param( ReleaseIdMeta(11, "11"), [TrackIdMeta(20000, "ISRC00", "11"), TrackIdMeta(20011, "ISRC11", "11")], generate_orchard_release, 11, { "system_errors": ["Error locating release from art_relations for 11."], "release": {}, "tracks": {}, }, ValidateInternalError, id="no AR release via side_effect", ), pytest.param( ReleaseIdMeta(11, "11"), [TrackIdMeta(20000, "ISRC00", "11"), TrackIdMeta(20011, "ISRC11", "11")], lambda _: None, 11, { "system_errors": ["Error locating release from art_relations for 11."], "release": {}, "tracks": {}, }, ValidateInternalError, id="no AR release via return_value", ), pytest.param( ReleaseIdMeta(0, "00"), [TrackIdMeta(20000, "ISRC00", ""), TrackIdMeta(20015, "ISRC15", "")], generate_orchard_release, 0, { "system_errors": [ "internal_error-Error retrieving track info for ISRC15." ], "release": {}, "tracks": {}, }, ValidateInternalError, id="missing AR track", ), pytest.param( ReleaseIdMeta(0, "00"), [], lambda _: generate_release(ReleaseIdMeta(0, "00"), True, True, True), 0, { "system_errors": [], "release": { "upc": "00", "product_no": "prod#_00", "grid": "grid#_00", "errors": {"metadata": [], "general": []}, }, "tracks": {}, }, None, id="no tracks physical release", ), pytest.param( ReleaseIdMeta(5, "5"), [TrackIdMeta(20000, "ISRC00", "5"), TrackIdMeta(20011, "ISRC11", "5")], generate_orchard_release, 5, { "system_errors": [], "release": {}, "tracks": { 20000: { "isrc": "ISRC00", "product_no": "prod#_5_ISRC00", "grid": "grid#_5_ISRC00", "errors": {"metadata": [], "general": []}, } }, }, None, id="no GRAS release", ), ], ) def test_validate( mocker: MockerFixture, release_id_meta: ReleaseIdMeta, track_list: list[TrackIdMeta], orchard_release_lookup: Any, validate_arg: int, expected_result: dict[str, Any], expected_exception: type[Exception] | None, ) -> None: """Test validate across happy path and error scenarios.""" mocker.patch.object( orchard, "lookup_release_identifiers", return_value=release_id_meta ) mocker.patch.object(orchard, "lookup_track_list", return_value=track_list) mocker.patch.object(orchard, "lookup_release", side_effect=orchard_release_lookup) mocker.patch.object(orchard, "lookup_track", side_effect=generate_orchard_track) mocker.patch.object(delphi, "lookup_release", side_effect=generate_gras_release) mocker.patch.object(delphi, "lookup_track", side_effect=generate_gras_track) if expected_exception: with pytest.raises(expected_exception) as exc: manager.validate(validate_arg) assert getattr(exc.value, "result") == expected_result else: assert manager.validate(validate_arg) == expected_result