"""Test graphql utils.""" from unittest import mock import pytest from gql.transport.exceptions import TransportQueryError, TransportServerError from requests.exceptions import HTTPError from soundrecording_utils.common.exceptions.exceptions import RetryableException from soundrecording_utils.ddex.utils import graphql @mock.patch('soundrecording_utils.ddex.utils.graphql._execute_tracks_query') def test_get_track_returns_primary_track_with_participations(mock_execute_query): """Returns the track whose TUID matches primary_tuid when it has participations.""" primary = { 'tuid': 'tuid-primary', 'trackName': 'Fire', 'labelSoundRecording': {'label': {'name': 'La Reserve Records'}}, 'product': {'subgenre': {'genre': {'name': 'Rock'}}}, 'participations': [ {'participant': {'name': 'Artist A', 'uuid': '1234'}, 'participated_as': 'performer'}, {'participant': {'name': 'Artist B', 'uuid': '5678'}, 'participated_as': 'track_writer'}, ], } other = { 'tuid': 'tuid-other', 'trackName': 'Fire (Alt)', 'labelSoundRecording': {'label': {'name': 'Other Label'}}, 'product': {'subgenre': {'genre': {'name': 'Rock'}}}, 'participations': [], } mock_execute_query.return_value = {'isrc': {'tracks': {'tracks': [primary, other]}}} result = graphql.get_track('QM6MZ2016828', 'tuid-primary', 'TestApp') assert mock_execute_query.called assert result == primary @mock.patch('soundrecording_utils.ddex.utils.graphql._execute_tracks_query') def test_get_track_falls_back_when_primary_has_no_participations(mock_execute_query): """Falls back to first track with participations when primary has none.""" primary = { 'tuid': 'tuid-primary', 'trackName': 'Untitled', 'labelSoundRecording': {'label': {'name': 'Label A'}}, 'product': {'subgenre': {'genre': {'name': 'Rock'}}}, 'participations': [], } with_participants = { 'tuid': 'tuid-other', 'trackName': 'Wipeout', 'labelSoundRecording': {'label': {'name': 'Label B'}}, 'product': {'subgenre': {'genre': {'name': 'Rock'}}}, 'participations': [ {'participant': {'name': 'Gary Hoey', 'uuid': 'uuid-1'}, 'participated_as': 'performer'}, ], } mock_execute_query.return_value = {'isrc': {'tracks': {'tracks': [primary, with_participants]}}} result = graphql.get_track('QM6MZ2016828', 'tuid-primary', 'TestApp') assert result == with_participants @mock.patch('soundrecording_utils.ddex.utils.graphql._execute_tracks_query') def test_get_track_returns_primary_when_no_track_has_participations(mock_execute_query): """Returns the primary track when no track has participations.""" primary = { 'tuid': 'tuid-primary', 'trackName': 'Track A', 'labelSoundRecording': {'label': {'name': 'Label'}}, 'product': {'subgenre': {'genre': {'name': 'Rock'}}}, 'participations': [], } mock_execute_query.return_value = {'isrc': {'tracks': {'tracks': [primary]}}} result = graphql.get_track('QM6MZ2016828', 'tuid-primary', 'TestApp') assert result == primary @mock.patch('soundrecording_utils.ddex.utils.graphql._execute_tracks_query') def test_get_track_returns_none_when_no_tracks(mock_execute_query): """Returns None when the ISRC query returns no tracks.""" mock_execute_query.return_value = {'isrc': {'tracks': {'tracks': []}}} result = graphql.get_track('QM6MZ2016828', 'tuid-primary', 'TestApp') assert result is None @mock.patch('soundrecording_utils.ddex.utils.graphql._execute_tracks_query') def test_get_track_exception(mock_execute_query): """Test exception tracks query.""" mock_execute_query.side_effect = HTTPError('error') with pytest.raises(HTTPError): graphql.get_track('QM6MZ2016828', 'tuid-123', 'TestApp') @pytest.mark.parametrize('status_code', [502, 503, 504]) @mock.patch('soundrecording_utils.ddex.utils.graphql._execute_tracks_query') def test_get_track_transport_server_error_retryable(mock_execute_query, status_code): """Test that retryable HTTP codes raise RetryableException.""" mock_execute_query.side_effect = TransportServerError( f'Server error {status_code}', code=status_code ) with pytest.raises(RetryableException, match=f'HTTP:{status_code}'): graphql.get_track('QM6MZ2016828', 'tuid-123', 'TestApp') @mock.patch('soundrecording_utils.ddex.utils.graphql._execute_tracks_query') def test_get_track_transport_server_error_non_retryable(mock_execute_query): """Test that non-retryable server errors are re-raised as-is.""" mock_execute_query.side_effect = TransportServerError( 'Internal Server Error', code=500 ) with pytest.raises(TransportServerError): graphql.get_track('QM6MZ2016828', 'tuid-123', 'TestApp') @mock.patch('soundrecording_utils.ddex.utils.graphql._execute_tracks_query') def test_get_track_transport_query_error(mock_execute_query): """Test that TransportQueryError raises RetryableException.""" mock_execute_query.side_effect = TransportQueryError('query failed') with pytest.raises(RetryableException, match='query failed'): graphql.get_track('QM6MZ2016828', 'tuid-123', 'TestApp') # --- get_primary_track_isrc_and_tuid --- def _osr_search_response(items): return { 'orchardSoundRecordingSearchByIsrc': [{ 'id': 'osr-uuid-1', 'isrc': 'OSR001', 'tracks': {'items': items}, }] } @mock.patch('soundrecording_utils.ddex.utils.graphql._execute_osr_search_by_isrc_query') def test_get_primary_track_tuid_returns_primary_track_tuid_and_isrc(mock_execute): """Returns the TUID and ISRC of the active primary track.""" mock_execute.return_value = _osr_search_response([ {'tuid': '1', 'isrc': 'TRACK001', 'active': True, 'primaryTrack': True}, {'tuid': '2', 'isrc': 'TRACK002', 'active': True, 'primaryTrack': False}, ]) result = graphql.get_primary_track_isrc_and_tuid('OSR001', 'TestApp') mock_execute.assert_called_once_with('OSR001', 'TestApp') assert result == ('TRACK001', '1') @mock.patch('soundrecording_utils.ddex.utils.graphql._execute_osr_search_by_isrc_query') def test_get_primary_track_tuid_falls_back_to_first_active(mock_execute): """Falls back to first active track TUID and ISRC when none is marked primary.""" mock_execute.return_value = _osr_search_response([ {'tuid': '1', 'isrc': 'TRACK001', 'active': False, 'primaryTrack': False}, {'tuid': '2', 'isrc': 'TRACK002', 'active': True, 'primaryTrack': False}, {'tuid': '3', 'isrc': 'TRACK003', 'active': True, 'primaryTrack': False}, ]) result = graphql.get_primary_track_isrc_and_tuid('OSR001', 'TestApp') assert result == ('TRACK002', '2') @mock.patch('soundrecording_utils.ddex.utils.graphql._execute_osr_search_by_isrc_query') def test_get_primary_track_isrc_and_tuid_returns_none_when_osr_not_found(mock_execute): """Returns None when OSR is not found for the given ISRC.""" mock_execute.return_value = {'orchardSoundRecordingSearchByIsrc': []} result = graphql.get_primary_track_isrc_and_tuid('OSR001', 'TestApp') assert result is None @mock.patch('soundrecording_utils.ddex.utils.graphql._execute_osr_search_by_isrc_query') def test_get_primary_track_isrc_and_tuid_returns_none_when_no_active_tracks(mock_execute): """Returns None when OSR has no active tracks.""" mock_execute.return_value = _osr_search_response([ {'tuid': '1', 'isrc': 'TRACK001', 'active': False, 'primaryTrack': False}, ]) result = graphql.get_primary_track_isrc_and_tuid('OSR001', 'TestApp') assert result is None @mock.patch('soundrecording_utils.ddex.utils.graphql._execute_osr_search_by_isrc_query') def test_get_primary_track_isrc_and_tuid_returns_none_when_no_tracks(mock_execute): """Returns None when OSR has an empty track list.""" mock_execute.return_value = _osr_search_response([]) result = graphql.get_primary_track_isrc_and_tuid('OSR001', 'TestApp') assert result is None @pytest.mark.parametrize('status_code', [502, 503, 504]) @mock.patch('soundrecording_utils.ddex.utils.graphql._execute_osr_search_by_isrc_query') def test_get_primary_track_isrc_and_tuid_retryable_server_error(mock_execute, status_code): """Retryable HTTP codes are wrapped in RetryableException.""" mock_execute.side_effect = TransportServerError( f'Server error {status_code}', code=status_code ) with pytest.raises(RetryableException, match=f'HTTP:{status_code}'): graphql.get_primary_track_isrc_and_tuid('OSR001', 'TestApp') @mock.patch('soundrecording_utils.ddex.utils.graphql._execute_osr_search_by_isrc_query') def test_get_primary_track_isrc_and_tuid_non_retryable_server_error(mock_execute): """Non-retryable server errors are re-raised as-is.""" mock_execute.side_effect = TransportServerError('Internal Server Error', code=500) with pytest.raises(TransportServerError): graphql.get_primary_track_isrc_and_tuid('OSR001', 'TestApp') @mock.patch('soundrecording_utils.ddex.utils.graphql._execute_osr_search_by_isrc_query') def test_get_primary_track_isrc_and_tuid_transport_query_error(mock_execute): """Wraps TransportQueryError in RetryableException.""" mock_execute.side_effect = TransportQueryError('query failed') with pytest.raises(RetryableException, match='query failed'): graphql.get_primary_track_isrc_and_tuid('OSR001', 'TestApp')