import unittest import sys import os import logging import dotenv import pytest from unittest.mock import patch, MagicMock # Add the project root to Python path for imports sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) # Create the helpers directory if it doesn't exist helpers_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'musicround', 'helpers') if not os.path.exists(helpers_dir): os.makedirs(helpers_dir) # Create __init__.py if it doesn't exist helpers_init = os.path.join(helpers_dir, '__init__.py') if not os.path.exists(helpers_init): with open(helpers_init, 'w') as f: f.write('# This file makes the helpers directory a proper Python package\n') from musicround.helpers.metadata import ( get_song_metadata_by_isrc, get_musicbrainz_data, get_spotify_data, get_deezer_data, get_lastfm_data, get_openai_data, get_acrcloud_data ) class TestMetadataHelper(unittest.TestCase): """Tests for the metadata helper functions.""" def setUp(self): """Set up common test resources.""" # Create a mock Flask app with logger self.mock_app = MagicMock() self.mock_app.logger = logging.getLogger("test_logger") self.mock_app.config = {} # Test ISRC for AC/DC's Highway to Hell self.test_isrc = "AUAP07900028" @patch('musicround.helpers.metadata.get_musicbrainz_data') @patch('musicround.helpers.metadata.get_spotify_data') @patch('musicround.helpers.metadata.get_deezer_data') @patch('musicround.helpers.metadata.get_lastfm_data') @patch('musicround.helpers.metadata.get_openai_data') @patch('musicround.helpers.metadata.get_acrcloud_data') # Add the new mock for ACRCloud def test_acdc_highway_to_hell(self, mock_acrcloud, mock_openai, mock_lastfm, mock_deezer, mock_spotify, mock_musicbrainz): """Test that AC/DC's Highway to Hell returns correct metadata.""" # Set up mock return values mock_musicbrainz.return_value = { "title": "Highway to Hell", "artist_name": "AC/DC", "year": "1979", "genre": "Hard Rock" } mock_spotify.return_value = { "title": "Highway to Hell", "artist_name": "AC/DC", "year": "1979", "genre": "Hard Rock", "popularity": 82 } mock_deezer.return_value = { "title": "Highway to Hell", "artist_name": "AC/DC", "year": "1979", "preview_url": "https://cdns-preview-8.dzcdn.net/stream/c-8f5a9d479bc54be17e2754cb9653e60b-6.mp3" } mock_lastfm.return_value = { "genre": "Classic Rock" } mock_openai.return_value = { "genre": "Rock", "year": "1979" } # Add ACRCloud mock data mock_acrcloud.return_value = { "title": "Highway to Hell", "artist_name": "AC/DC", "year": "1979", "genre": ["Rock", "Hard Rock"], "spotify_id": "2zYzyRzz6pRmhPzyfMEC8s", "deezer_id": "89077521", "spotify_preview_url": "https://audio-ssl.spotify.com/preview/track1234.mp3" } # Call function with test ISRC metadata = get_song_metadata_by_isrc(self.test_isrc, self.mock_app) # Verify mock functions were called correctly mock_acrcloud.assert_called_once_with(self.test_isrc, self.mock_app) mock_musicbrainz.assert_called_once_with(self.test_isrc, self.mock_app.logger) mock_spotify.assert_called_once_with(self.test_isrc, self.mock_app) mock_deezer.assert_called_once_with(self.test_isrc, self.mock_app) # LastFM and OpenAI should be called with title and artist name mock_lastfm.assert_called_once_with("AC/DC", "Highway to Hell", self.mock_app) mock_openai.assert_called_once_with("AC/DC", "Highway to Hell", self.mock_app) # Assert the expected metadata values self.assertEqual(metadata["title"], "Highway to Hell") self.assertEqual(metadata["artist_name"], "AC/DC") self.assertEqual(metadata["year"], "1979") # Genre should be the most common one from all sources self.assertEqual(metadata["genre"], "Hard Rock") # Hard Rock appears most frequently in our mocks self.assertEqual(metadata["popularity"], 82) # Preview URL should be from Spotify since we prioritize it self.assertEqual(metadata["preview_url"], "https://audio-ssl.spotify.com/preview/track1234.mp3") # Check that ACRCloud platform IDs are present self.assertEqual(metadata["spotify_id"], "2zYzyRzz6pRmhPzyfMEC8s") self.assertEqual(metadata["deezer_id"], "89077521") # Check that all sources are included expected_sources = ["acrcloud", "musicbrainz", "spotify", "deezer", "lastfm", "openai"] self.assertCountEqual(metadata["sources"], expected_sources) @patch('musicround.helpers.metadata.requests.get') @patch('musicround.helpers.metadata.musicbrainzngs.search_recordings') def test_integration_with_real_data(self, mock_mb_search, mock_requests): """ Test with more realistic data structures that might be returned by APIs. This is still a mock test but with more complex structures. """ # Set up MockResponse class for requests class MockResponse: def __init__(self, json_data, status_code): self.json_data = json_data self.status_code = status_code def json(self): return self.json_data # Mock MusicBrainz data mock_mb_search.return_value = { 'recording-list': [{ 'title': 'Highway to Hell', 'artist-credit': [ {'artist': {'name': 'AC/DC'}} ], 'tag-list': [ {'name': 'hard rock'}, {'name': 'rock'}, {'name': 'classic rock'} ], 'release-list': [ {'date': '1979-07-27'} ] }] } # Mock Last.fm response mock_lastfm_response = { 'track': { 'name': 'Highway to Hell', 'artist': {'name': 'AC/DC'}, 'toptags': { 'tag': [ {'name': 'rock'}, {'name': 'classic rock'}, {'name': 'hard rock'} ] } } } # Set up the mock for requests.get to return the mock response def mock_request_get(url, **kwargs): if 'audioscrobbler.com' in url: return MockResponse(mock_lastfm_response, 200) return MockResponse({}, 404) mock_requests.side_effect = mock_request_get # Since we can't easily mock everything, we'll patch the other functions with patch('musicround.helpers.metadata.get_spotify_data') as mock_spotify, \ patch('musicround.helpers.metadata.get_deezer_data') as mock_deezer, \ patch('musicround.helpers.metadata.get_openai_data') as mock_openai: mock_spotify.return_value = {} mock_deezer.return_value = {} mock_openai.return_value = {} # Only test MusicBrainz data in this test metadata = get_song_metadata_by_isrc(self.test_isrc, self.mock_app) # Verify the results self.assertEqual(metadata["title"], "Highway to Hell") self.assertEqual(metadata["artist_name"], "AC/DC") self.assertEqual(metadata["year"], "1979") self.assertEqual(metadata["genre"], "hard rock") # First tag from tag-list @pytest.mark.skip(reason="Integration test requiring live API credentials. Run manually when needed.") def test_real_api_calls(self): """ Test actual API calls with AC/DC's Highway to Hell. This test makes real API calls so it should be skipped by default. Run this test manually when needed by removing the skip decorator. """ # Load environment variables from .env file dotenv.load_dotenv(os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env')) # Initialize test app with valid API keys real_app = MagicMock() real_app.logger = logging.getLogger("real_api_test") real_app.logger.setLevel(logging.INFO) # Add a console handler to see the logs console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) real_app.logger.addHandler(console_handler) # Set up config with actual API keys from .env file real_app.config = { 'LASTFM_API_KEY': os.environ.get('LASTFM_API_KEY'), 'OPENAI_API_KEY': os.environ.get('OPENAI_API_KEY'), 'OPENAI_MODEL': os.environ.get('OPENAI_MODEL', 'gpt-4o-mini'), 'OPENAI_URL': os.environ.get('OPENAI_URL'), 'ACRCLOUD_TOKEN': os.environ.get('ACRCLOUD_TOKEN'), # Add ACRCloud token } # Setup Spotify client if credentials are available if all([os.environ.get(key) for key in ['SPOTIFY_CLIENT_ID', 'SPOTIFY_CLIENT_SECRET']]): import spotipy from spotipy.oauth2 import SpotifyClientCredentials real_app.logger.info("Setting up Spotify client...") spotify_client_credentials = SpotifyClientCredentials( client_id=os.environ.get('SPOTIFY_CLIENT_ID'), client_secret=os.environ.get('SPOTIFY_CLIENT_SECRET') ) real_app.config['sp'] = spotipy.Spotify(client_credentials_manager=spotify_client_credentials) # Setup Deezer client from musicround.deezer_client import DeezerClient real_app.config['deezer'] = DeezerClient() # Test MusicBrainz directly (doesn't need API keys) real_app.logger.info("Testing MusicBrainz API...") mb_data = get_musicbrainz_data(self.test_isrc, real_app.logger) self.assertIsNotNone(mb_data) if mb_data: real_app.logger.info(f"MusicBrainz data: {mb_data}") self.assertEqual(mb_data.get("title"), "Highway to Hell") self.assertIn("AC/DC", mb_data.get("artist_name", "")) # Test Spotify if client is available if real_app.config.get('sp'): real_app.logger.info("Testing Spotify API...") spotify_data = get_spotify_data(self.test_isrc, real_app) real_app.logger.info(f"Spotify data: {spotify_data}") self.assertIsNotNone(spotify_data) if spotify_data: self.assertEqual(spotify_data.get("title"), "Highway to Hell") self.assertIn("AC/DC", spotify_data.get("artist_name", "")) else: real_app.logger.warning("Skipping Spotify test: credentials not available") # Test Deezer API real_app.logger.info("Testing Deezer API...") deezer_data = get_deezer_data(self.test_isrc, real_app) real_app.logger.info(f"Deezer data: {deezer_data}") if deezer_data: self.assertIsNotNone(deezer_data) if "title" in deezer_data: self.assertEqual(deezer_data.get("title"), "Highway to Hell") if "artist_name" in deezer_data: self.assertIn("AC/DC", deezer_data.get("artist_name")) # Test Last.fm if API key is available if real_app.config.get('LASTFM_API_KEY'): real_app.logger.info("Testing Last.fm API...") lastfm_data = get_lastfm_data("AC/DC", "Highway to Hell", real_app) real_app.logger.info(f"Last.fm data: {lastfm_data}") self.assertIsNotNone(lastfm_data) if lastfm_data: self.assertIn("genre", lastfm_data) else: real_app.logger.warning("Skipping Last.fm test: API key not available") # Test OpenAI if API key is available if real_app.config.get('OPENAI_API_KEY'): real_app.logger.info("Testing OpenAI API...") openai_data = get_openai_data("AC/DC", "Highway to Hell", real_app) real_app.logger.info(f"OpenAI data: {openai_data}") self.assertIsNotNone(openai_data) if openai_data: self.assertIn("year", openai_data) # Convert to string for comparison if it's an integer expected_year = "1979" actual_year = str(openai_data.get("year")) if openai_data.get("year") is not None else None self.assertEqual(actual_year, expected_year) else: real_app.logger.warning("Skipping OpenAI test: API key not available") # Test ACRCloud if token is available if real_app.config.get('ACRCLOUD_TOKEN'): from musicround.helpers.metadata import get_acrcloud_data real_app.logger.info("Testing ACRCloud API...") acrcloud_data = get_acrcloud_data(self.test_isrc, real_app) real_app.logger.info(f"ACRCloud data: {acrcloud_data}") if acrcloud_data: self.assertIsNotNone(acrcloud_data) if "title" in acrcloud_data: self.assertEqual(acrcloud_data.get("title"), "Highway to Hell") if "artist_name" in acrcloud_data: self.assertIn("AC/DC", acrcloud_data.get("artist_name")) # Check if platform IDs were retrieved if "spotify_id" in acrcloud_data: self.assertIsNotNone(acrcloud_data.get("spotify_id")) if "deezer_id" in acrcloud_data: self.assertIsNotNone(acrcloud_data.get("deezer_id")) else: real_app.logger.warning("Skipping ACRCloud test: API token not available") # Full test with all APIs real_app.logger.info("Testing full metadata aggregation...") metadata = get_song_metadata_by_isrc(self.test_isrc, real_app) real_app.logger.info(f"Full metadata: {metadata}") self.assertIsNotNone(metadata) if metadata: self.assertEqual(metadata.get("title"), "Highway to Hell") self.assertIn("AC/DC", metadata.get("artist_name", "")) self.assertEqual(metadata.get("year"), "1979") self.assertIsNotNone(metadata.get("genre")) self.assertIn("musicbrainz", metadata.get("sources", [])) # Check if we got data from other sources as expected if real_app.config.get('ACRCLOUD_TOKEN'): self.assertIn("acrcloud", metadata.get("sources", [])) if real_app.config.get('sp'): self.assertIn("spotify", metadata.get("sources", [])) if real_app.config.get('LASTFM_API_KEY'): self.assertIn("lastfm", metadata.get("sources", [])) # Only check for OpenAI if we have both key and we've verified it works if real_app.config.get('OPENAI_API_KEY') and openai_data and ('genre' in openai_data or 'year' in openai_data): self.assertIn("openai", metadata.get("sources", [])) if __name__ == '__main__': unittest.main()