Files

395 lines
15 KiB
Python
Raw Permalink Normal View History

"""
test_download_duplicate_prevention.py
Test suite for download duplicate prevention functionality.
Tests cover:
- Duplicate prevention during downloads
- Integration with HistoryService
- Skipped item logging
- Settings toggle behavior
"""
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from tidalapi import Quality, Track
from tidal_dl_ng.download import Download
from tidal_dl_ng.history import HistoryService
@pytest.fixture
def mock_tidal():
"""Create a mock Tidal object for testing.
Returns:
Mock Tidal object with session.
"""
mock = MagicMock()
mock.session = MagicMock()
mock.session.audio_quality = Quality.high_lossless
mock.switch_to_atmos_session = MagicMock(return_value=True)
mock.restore_normal_session = MagicMock(return_value=True)
mock.stream_lock = MagicMock()
mock.stream_lock.__enter__ = MagicMock(return_value=None)
mock.stream_lock.__exit__ = MagicMock(return_value=None)
mock.settings = MagicMock()
mock.settings.data = MagicMock()
return mock
@pytest.fixture
def mock_logger():
"""Create a mock logger for testing.
Returns:
Mock logger object.
"""
logger = MagicMock()
logger.info = MagicMock()
logger.debug = MagicMock()
logger.warning = MagicMock()
logger.error = MagicMock()
return logger
@pytest.fixture
def temp_download_path(tmp_path: Path) -> Path:
"""Create a temporary download directory.
Args:
tmp_path: pytest fixture providing temporary directory.
Returns:
Path to temporary download directory.
"""
download_dir = tmp_path / "downloads"
download_dir.mkdir()
return download_dir
@pytest.fixture
def history_service_with_temp(tmp_path: Path, monkeypatch) -> HistoryService:
"""Create a HistoryService with temporary storage.
Args:
tmp_path: pytest fixture providing temporary directory.
monkeypatch: pytest monkeypatch fixture.
Returns:
Configured HistoryService instance.
"""
config_path = tmp_path / "config"
config_path.mkdir(parents=True, exist_ok=True)
monkeypatch.setattr("tidal_dl_ng.history.path_config_base", lambda: str(config_path))
# Reset singleton
if hasattr(HistoryService, "_instances"):
HistoryService._instances = {}
return HistoryService()
@pytest.fixture
def download_instance(mock_tidal, mock_logger, temp_download_path, history_service_with_temp):
"""Create a Download instance for testing.
Args:
mock_tidal: Mock Tidal object.
mock_logger: Mock logger.
temp_download_path: Temporary download directory.
history_service_with_temp: HistoryService with temp storage.
Returns:
Configured Download instance.
"""
with patch("tidal_dl_ng.download.HistoryService", return_value=history_service_with_temp):
download = Download(
tidal_obj=mock_tidal, path_base=str(temp_download_path), fn_logger=mock_logger, skip_existing=False
)
return download
class TestDuplicatePreventionInDownload:
"""Test duplicate prevention during download operations."""
def test_skip_download_when_track_in_history_and_prevention_enabled(self, download_instance: Download, mock_logger):
"""Test that download is skipped when track is in history and prevention is enabled."""
# Add track to history
track_id = "123456"
download_instance.history_service.add_track_to_history(track_id)
download_instance.history_service.update_settings(preventDuplicates=True)
# Create mock track
mock_track = MagicMock(spec=Track)
mock_track.id = int(track_id)
mock_track.name = "Test Track"
mock_track.available = True
mock_track.album = MagicMock()
mock_track.album.name = "Test Album"
# Attempt download
with patch.object(download_instance.session, "track", return_value=mock_track):
result, path = download_instance.item(file_template="{artist_name} - {track_title}", media=mock_track)
# Should be skipped (result False means skipped)
assert result is False
# Logger should have logged the skip message
assert mock_logger.info.called
skip_call_found = any(
"Skipped item" in str(call) or "already in history" in str(call) for call in mock_logger.info.call_args_list
)
assert skip_call_found
def test_download_proceeds_when_prevention_disabled(self, download_instance: Download, mock_logger):
"""Test that download proceeds when prevention is disabled."""
# Add track to history but disable prevention
track_id = "789012"
download_instance.history_service.add_track_to_history(track_id)
download_instance.history_service.update_settings(preventDuplicates=False)
# Create mock track
mock_track = MagicMock(spec=Track)
mock_track.id = int(track_id)
mock_track.name = "Test Track 2"
mock_track.available = True
# Mock the download process to avoid actual download
with (
patch.object(download_instance, "_validate_and_prepare_media", return_value=mock_track),
patch.object(download_instance, "_prepare_file_paths_and_skip_logic") as mock_paths,
):
mock_paths.return_value = (Path("/fake/path.flac"), ".flac", False, False)
# The download should not be skipped by duplicate prevention
# (it will still fail later in the chain, but that's okay for this test)
result = download_instance.history_service.should_skip_download(track_id)
# Should NOT be skipped
assert result is False
def test_download_proceeds_when_track_not_in_history(self, download_instance: Download):
"""Test that download proceeds when track is not in history."""
track_id = "999999"
download_instance.history_service.update_settings(preventDuplicates=True)
# Track not in history
result = download_instance.history_service.should_skip_download(track_id)
assert result is False
class TestHistoryLoggingMessages:
"""Test logging messages for skipped downloads."""
def test_skip_message_format(self, download_instance: Download, mock_logger):
"""Test that skip message follows the correct format."""
track_id = "111222"
download_instance.history_service.add_track_to_history(track_id)
download_instance.history_service.update_settings(preventDuplicates=True)
mock_track = MagicMock(spec=Track)
mock_track.id = int(track_id)
mock_track.name = "Amazing Song"
mock_track.artist = MagicMock()
mock_track.artist.name = "Great Artist"
mock_track.available = True
mock_track.album = MagicMock()
with patch.object(download_instance.session, "track", return_value=mock_track):
download_instance.item(file_template="{artist_name} - {track_title}", media=mock_track)
# Check that info logger was called
assert mock_logger.info.called
# Verify message contains expected parts
log_calls = [str(call) for call in mock_logger.info.call_args_list]
skip_message_found = any("Skipped item" in msg and "already in history" in msg for msg in log_calls)
assert skip_message_found
class TestHistoryAfterDownload:
"""Test that successful downloads are added to history."""
@patch("tidal_dl_ng.download.Download._download_and_process_media")
@patch("tidal_dl_ng.download.Download._adjust_quality_settings")
@patch("tidal_dl_ng.download.Download._perform_post_processing")
def test_track_added_to_history_after_successful_download(
self, mock_post_process, mock_quality, mock_download_process, download_instance: Download
):
"""Test that a track is added to history after successful download."""
# Mock successful download
mock_download_process.return_value = True
mock_quality.return_value = (None, None)
track_id = "333444"
mock_track = MagicMock(spec=Track)
mock_track.id = int(track_id)
mock_track.name = "New Track"
mock_track.available = True
mock_track.album = MagicMock()
with (
patch.object(download_instance, "_validate_and_prepare_media", return_value=mock_track),
patch.object(download_instance, "_prepare_file_paths_and_skip_logic") as mock_paths,
):
mock_paths.return_value = (Path("/fake/path.flac"), ".flac", False, False)
download_instance.item(
file_template="{artist_name} - {track_title}",
media=mock_track,
source_type="playlist",
source_id="pl-123",
source_name="Test Playlist",
)
# Track should be in history
assert download_instance.history_service.is_downloaded(track_id)
# Check metadata
track_info = download_instance.history_service.get_track_info(track_id)
assert track_info is not None
assert track_info["sourceType"] == "playlist"
assert track_info["sourceId"] == "pl-123"
@patch("tidal_dl_ng.download.Download._download_and_process_media")
def test_track_not_added_on_failed_download(self, mock_download_process, download_instance: Download):
"""Test that track is NOT added to history if download fails."""
# Mock failed download
mock_download_process.return_value = False
track_id = "555666"
mock_track = MagicMock(spec=Track)
mock_track.id = int(track_id)
mock_track.available = True
mock_track.album = MagicMock()
with (
patch.object(download_instance, "_validate_and_prepare_media", return_value=mock_track),
patch.object(download_instance, "_prepare_file_paths_and_skip_logic") as mock_paths,
patch.object(download_instance, "_adjust_quality_settings") as mock_quality,
):
mock_paths.return_value = (Path("/fake/path.flac"), ".flac", False, False)
mock_quality.return_value = (None, None)
download_instance.item(file_template="{artist_name} - {track_title}", media=mock_track)
# Track should NOT be in history
assert not download_instance.history_service.is_downloaded(track_id)
class TestSettingsIntegration:
"""Test integration with settings toggle."""
def test_settings_toggle_affects_duplicate_check(self, download_instance: Download):
"""Test that toggling settings immediately affects duplicate checking."""
track_id = "777888"
download_instance.history_service.add_track_to_history(track_id)
# Enable prevention
download_instance.history_service.update_settings(preventDuplicates=True)
assert download_instance.history_service.should_skip_download(track_id) is True
# Disable prevention
download_instance.history_service.update_settings(preventDuplicates=False)
assert download_instance.history_service.should_skip_download(track_id) is False
# Re-enable
download_instance.history_service.update_settings(preventDuplicates=True)
assert download_instance.history_service.should_skip_download(track_id) is True
def test_settings_persist_across_service_instances(self, tmp_path: Path, monkeypatch):
"""Test that settings persist across service instances."""
config_path = tmp_path / "config"
config_path.mkdir(parents=True, exist_ok=True)
monkeypatch.setattr("tidal_dl_ng.history.path_config_base", lambda: str(config_path))
# Create first instance and disable prevention
if hasattr(HistoryService, "_instances"):
HistoryService._instances = {}
service1 = HistoryService()
service1.update_settings(preventDuplicates=False)
# Create second instance (simulating app restart)
HistoryService._instances = {}
service2 = HistoryService()
# Settings should persist
assert service2.get_settings()["preventDuplicates"] is False
class TestVideoNotAffectedByHistory:
"""Test that video downloads are not affected by history (only tracks)."""
def test_video_download_not_checked_against_history(self, download_instance: Download):
"""Test that videos are not checked against download history."""
from tidalapi import Video
# This test verifies the type check in the duplicate prevention code
# Videos should bypass the history check entirely
mock_video = MagicMock(spec=Video)
mock_video.id = 123456
mock_video.name = "Test Video"
mock_video.available = True
# Even if we somehow had a video ID in history, it shouldn't matter
# because the check is `isinstance(media, Track)`
download_instance.history_service.add_track_to_history("123456")
with (
patch.object(download_instance, "_validate_and_prepare_media", return_value=mock_video),
patch.object(download_instance, "_prepare_file_paths_and_skip_logic") as mock_paths,
patch.object(download_instance.history_service, "should_skip_download") as mock_skip,
):
mock_paths.return_value = (Path("/fake.mp4"), ".mp4", False, False)
# The key is that should_skip_download is never called for videos
# Expected to fail, we're just checking the skip wasn't called
from contextlib import suppress
with suppress(Exception):
download_instance.item(file_template="{artist_name} - {track_title}", media=mock_video)
# should_skip_download should NOT have been called for video
assert not mock_skip.called
class TestBulkDownloadWithHistory:
"""Test history tracking in bulk download scenarios."""
@patch("tidal_dl_ng.download.Download.item")
def test_multiple_tracks_added_to_history(self, mock_item_download, download_instance: Download):
"""Test that multiple tracks in a bulk download are tracked."""
# Simulate successful downloads
mock_item_download.return_value = (True, Path("/fake/path.flac"))
track_ids = ["t1", "t2", "t3", "t4", "t5"]
# Manually add tracks to history (simulating what item() would do)
for track_id in track_ids:
download_instance.history_service.add_track_to_history(
track_id=track_id, source_type="album", source_id="al-123", source_name="Test Album"
)
# All tracks should be in history
for track_id in track_ids:
assert download_instance.history_service.is_downloaded(track_id)
# Should be grouped under same album
grouped = download_instance.history_service.get_history_by_source()
assert "album_al-123" in grouped
assert len(grouped["album_al-123"]) == 5
if __name__ == "__main__":
pytest.main([__file__, "-v"])