"""Unit tests for stores logic layer.""" from concurrent import futures from unittest import mock from unittest.mock import call from unittest.mock import MagicMock from unittest.mock import Mock from oto import response as oto_response import pytest from analytics.consts import error from analytics.logic import stores @pytest.fixture() def playlist_links(): """Return playlist links for testing.""" return [ 'http://open.spotify.com/playlist/37i9dQZF1DWWOaP4H0w5b0', ('https://itunes.apple.com/us/playlist/' 'idpl.25ccede448f045deb022c4b6ef7eed5b') ] @pytest.fixture def flask_g_mock(monkeypatch): """Mock Flask g object.""" flask_g = mock.Mock() monkeypatch.setattr(stores, 'g', flask_g) return flask_g def test_playlist_metadata_cache_key_case_insensitive(): """Test playlist_metadata cache key is case insensitive.""" uppercase_key = stores._get_playlist_metadata_cache_key('FOO') titlecase_key = stores._get_playlist_metadata_cache_key('Foo') lowercase_key = stores._get_playlist_metadata_cache_key('foo') assert uppercase_key == titlecase_key == lowercase_key @pytest.fixture(name='fake_playlist_metadata') def get_fake_metadata(): """Get fake playlist metadata.""" return { 'playlist_link': 'https://open.spotify.com/' 'playlist/5FAKE5PLAYLIST3ID6', 'playlist_title': 'Fake Playlist Title', 'playlist_author': 'Fake User', 'image': 'https://i.scdn.co/image/543fake423image43url0606', 'followers': 82000, 'status_code': 200, 'display_link': 'https://open.spotify.com/' 'user/fakeuserid/playlist/5FAKE5PLAYLIST3ID6', } def test_playlist_metadata_is_cached(fake_playlist_metadata): """Test _get_cached_playlist_metadata returns cached value.""" playlist_url = fake_playlist_metadata['playlist_link'] urls_dict = {playlist_url: fake_playlist_metadata} stores._cache_playlists_metadata(urls_dict) assert stores._get_cached_playlist_metadata([playlist_url]) == urls_dict def test_get_cached_playlist_metadata_returns_none_for_uncached_urls( fake_playlist_metadata): """Test _get_cached_playlist_metadata returns None for uncached urls.""" playlist_url = fake_playlist_metadata['playlist_link'] not_cached_url = 'not_cached' stores._cache_playlists_metadata({playlist_url: fake_playlist_metadata}) cache_response = stores._get_cached_playlist_metadata( [playlist_url, not_cached_url]) assert cache_response == { playlist_url: fake_playlist_metadata, not_cached_url: None } def test_split_playlist_links(): """Test _split_playlist_links helper function.""" spotify_link = 'https://open.spotify.com/playlist/A2OCSPCPO5Q' apple_playlist_link = 'https://itunes.apple.com/us/playlist/idpl.0001620d8' apple_station_link = 'https://itunes.apple.com/us/station/idra.5spotify' spotify_personalized_link = 'spotify-personalized-playlist:discoverWeekly' playlists = [ spotify_link, apple_playlist_link, apple_station_link, spotify_personalized_link] expected_result = { 'spotify': [{ 'playlist_id': 'A2OCSPCPO5Q', 'playlist_link': spotify_link}], 'apple_playlist': [{ 'playlist_id': 'pl.0001620d8', 'store': 'us', 'type': 'playlist', 'playlist_link': apple_playlist_link}], 'apple_station': [{ 'playlist_id': 'ra.5spotify', 'playlist_link': apple_station_link, 'store': 'us', 'type': 'station'}], 'spotify_personalized_playlist': [{ 'playlist_id': 'discoverWeekly', 'playlist_link': spotify_personalized_link}] } result = stores._split_playlist_links_by_stores(playlists) assert result == expected_result def test_prepare_result(): """Test _prepare_result function generates expected data format.""" response1 = Mock() response1.message = [{'playlist_link': 'v11'}, {'playlist_link': 'v12'}] response2 = Mock() response2.message = [{'playlist_link': 'v21'}, {'playlist_link': 'v22'}] data = [response1, response2] expected_result = { 'v11': {'playlist_link': 'v11'}, 'v12': {'playlist_link': 'v12'}, 'v21': {'playlist_link': 'v21'}, 'v22': {'playlist_link': 'v22'}} result = stores._prepare_result(data) assert result == expected_result @pytest.fixture() def mock_handlers(): """Mock get_playlist_metadata functions.""" m_handlers = {} for store in stores.store_handlers: _store_handler = MagicMock( name=store, return_value=oto_response.Response([])) stores.store_handlers[store]['handler'] = _store_handler m_handlers.update({store: _store_handler}) return m_handlers def test_fetch_playlists_metadata( monkeypatch, flask_g_mock, mock_handlers, mock_execute_in_parallel): """Test fetch_playlists_metadata successful execution.""" playlist_links = [] split_links = Mock() apple_playlist_links = Mock() apple_station_links = Mock() spotify_links = Mock() spotify_personalized_links = Mock() split_links_result = { 'apple_playlist': apple_playlist_links, 'apple_station': apple_station_links, 'spotify': spotify_links, 'spotify_personalized_playlist': spotify_personalized_links } split_links.return_value = split_links_result monkeypatch.setattr(stores, '_split_playlist_links_by_stores', split_links) valid_links = Mock(return_value=True) monkeypatch.setattr(stores, '_playlist_links_valid', valid_links) monkeypatch.setattr(futures, 'as_completed', MagicMock()) prepare_result = Mock() expected_result = prepare_result.return_value monkeypatch.setattr(stores, '_prepare_result', prepare_result) result = stores.fetch_playlists_metadata( playlist_links, [1, 2], ['theorchard']) mock_execute_in_parallel.executor.submit.assert_has_calls([ call( mock_handlers['apple_playlist'], apple_playlist_links, logger=flask_g_mock.log, feed_ids=[1, 2], distributors=['theorchard'] ), call( mock_handlers['apple_station'], apple_station_links, logger=flask_g_mock.log, feed_ids=[1, 2], distributors=['theorchard'] ), call( mock_handlers['spotify'], spotify_links, logger=flask_g_mock.log, feed_ids=[1, 2], distributors=['theorchard'] ), call( mock_handlers['spotify_personalized_playlist'], spotify_personalized_links, logger=flask_g_mock.log, feed_ids=[1, 2], distributors=['theorchard'] ) ], any_order=True) assert result.message == expected_result def test_fetch_playlists_metadata_failed_split( monkeypatch, flask_g_mock, mock_handlers): """Test fetch_playlists_metadata with wrong format links.""" playlist_links = [] split_links = Mock() monkeypatch.setattr(stores, '_split_playlist_links_by_stores', split_links) valid_links = Mock(return_value=False) monkeypatch.setattr(stores, '_playlist_links_valid', valid_links) result = stores.fetch_playlists_metadata( playlist_links, [1, 2], ['theorchard']) assert result.errors == error.INVALID_LINK_FORMAT_MESAGE def _assert_metadata_lists_equal(a, b, *, message='Lists are not equal'): """Assert metadata lists are equal, ignore order. Args: a (list[dict]): First list. b (list[dict]): Second list. message (str): Assertion error message. """ def _metadata_sort_key(metadata_dict): return metadata_dict['playlist_link'] assert ( sorted(a, key=_metadata_sort_key) == sorted(b, key=_metadata_sort_key)), message def test_get_playlists_metadata_fetches_uncached_only( fake_playlist_metadata, monkeypatch): """Test get_playlists_metadata fetches only uncached playlists.""" playlist_url = fake_playlist_metadata['playlist_link'] stores._cache_playlists_metadata({playlist_url: fake_playlist_metadata}) not_cached_url = 'not_cached' not_cached_metadata = { 'key': 'value', 'playlist_link': not_cached_url} fetch_mock = MagicMock( return_value=oto_response.Response( {not_cached_url: not_cached_metadata})) monkeypatch.setattr(stores, 'fetch_playlists_metadata', fetch_mock) response = stores.get_playlists_metadata( [playlist_url, not_cached_url], [1, 2], ['theorchard']) _assert_metadata_lists_equal( response.message, [fake_playlist_metadata, not_cached_metadata]) fetch_mock.assert_called_once_with(['not_cached'], [1, 2], ['theorchard']) def test_get_playlists_metadata_skips_fetch_if_all_cached( fake_playlist_metadata, monkeypatch): """Test get_playlists_metadata skips fetch if all playlists are cached.""" playlist_url = fake_playlist_metadata['playlist_link'] stores._cache_playlists_metadata({playlist_url: fake_playlist_metadata}) fetch_mock = MagicMock() monkeypatch.setattr(stores, 'fetch_playlists_metadata', fetch_mock) response = stores.get_playlists_metadata( [playlist_url], [1, 2], ['theorchard']) _assert_metadata_lists_equal( response.message, [fake_playlist_metadata]) fetch_mock.assert_not_called() def test_get_playlists_metadata_returns_error_response_on_error( fake_playlist_metadata, monkeypatch): """Test get_playlists_metadata returns error from fetch.""" playlist_url = fake_playlist_metadata['playlist_link'] error_response = oto_response.create_error_response( code=42, message='Test message') fetch_mock = MagicMock(return_value=error_response) monkeypatch.setattr(stores, 'fetch_playlists_metadata', fetch_mock) response = stores.get_playlists_metadata( [playlist_url], [1, 2], ['theorchard']) assert response is error_response @pytest.mark.parametrize('bad_status_code', [401, 429, 500, 502]) def test_cache_playlists_metadata_does_not_cache_unknown_statuses( bad_status_code, fake_playlist_metadata): """Test _cache_playlists_metadata caches only whitelisted status codes.""" ok_playlist_url = fake_playlist_metadata['playlist_link'] bad_playlist_url = 'trotteled_url' bad_playlist_metadata = { 'playlist_link': bad_playlist_url, 'status_code': bad_status_code} stores._cache_playlists_metadata({ ok_playlist_url: fake_playlist_metadata, bad_playlist_url: bad_playlist_metadata}) cached_metadata = stores._get_cached_playlist_metadata( [ok_playlist_url, bad_playlist_url]) assert cached_metadata == { bad_playlist_url: None, ok_playlist_url: fake_playlist_metadata} @pytest.mark.parametrize('playlist_link,expected_store', [ ( 'https://music.amazon.com/playlists/1501483614535', 'amazon_music'), ( 'http://music.amazon.com/stations/A1N5A7AN7SOT1T', 'amazon_music'), ( 'nohref://amazon_music/GNO_UNL_PLAYLIST/AZKAGPPWORIRY', 'amazon_music'), ( 'http://open.spotify.com/user/spotify/' 'playlist/37i9dQZF1DWZQ2i9kCiPaY', 'spotify'), ( 'https://open.spotify.com/user/brazilian/' 'playlist/2bwJT9JHKFJHE5s6JZB53c', 'spotify'), ( 'https://open.spotify.com/' 'playlist/2bwJT9JHKFJHE5s6JZB53c', 'spotify'), ( 'http://open.spotify.com/' 'playlist/HF7KDSA77SA5', 'spotify'), ( 'https://itunes.apple.com/us/station/idra.985499979', 'apple_station'), ( 'http://itunes.apple.com/us/playlist/' 'idpl.d8256d9129964c438171082ebb232669', 'apple_playlist'), ( 'http://itunes-apple-com/us/playlist/' 'idpl.d8256d9129964c438171082ebb232669', None), ( 'spotify-personalized-playlist:discoverWeekly', 'spotify_personalized_playlist'), ( 'spotify-personalized-playlist:invalid_playlist', 'spotify_personalized_playlist'), ]) def test_playlist_matches_with_store( playlist_link, expected_store, monkeypatch): """Test _match_playlist_with_store matches link with expected store.""" store, _ = stores._match_playlist_with_store(playlist_link) assert store == expected_store