Files
copilot-swe-agent[bot] cfe000f030 fix: resolve logical errors, bugs, and security issues across codebase
- Fix is_admin() method shadowing is_admin database column in User model
- Fix check_password() crash when password_hash is None (OAuth-only users)
- Fix SystemSetting.all_settings() formatting error (missing newline)
- Fix MAIL_PORT returning string instead of int in config
- Fix AUTOMATION_TOKEN config formatting (missing newline before comment)
- Fix path traversal vulnerability in serve_user_audio using realpath validation
- Fix weak auth in process.py, replace session check with @login_required
- Fix int() crash on non-numeric priority in import_songs.py
- Add timeout to SMTP connection in email_helper.py
- Add timeouts to external API requests in metadata.py and spotify_helper.py
- Fix security tests to properly reload config module
- Fix metadata test mock data key mismatch (preview_url -> spotify_preview_url)
- Add skip decorator to integration test requiring live API credentials

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
2026-03-12 00:13:30 +00:00

358 lines
16 KiB
Python

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()