import asyncio import logging from unittest.mock import AsyncMock, MagicMock import pytest from services.audiodb_browse_queue import AudioDBBrowseQueue, BrowseQueueItem @pytest.fixture def queue(): return AudioDBBrowseQueue() @pytest.fixture def mock_audiodb_svc(): svc = AsyncMock() svc.fetch_and_cache_artist_images = AsyncMock(return_value=None) svc.fetch_and_cache_album_images = AsyncMock(return_value=None) return svc @pytest.fixture def mock_prefs(): prefs = MagicMock() settings = MagicMock() settings.audiodb_enabled = True prefs.get_advanced_settings.return_value = settings return prefs class TestEnqueue: @pytest.mark.asyncio async def test_enqueue_adds_item(self, queue: AudioDBBrowseQueue): await queue.enqueue("artist", "abc-123", name="Coldplay") assert queue._queue.qsize() == 1 @pytest.mark.asyncio async def test_enqueue_dedup_same_mbid(self, queue: AudioDBBrowseQueue): await queue.enqueue("artist", "abc-123") await queue.enqueue("artist", "abc-123") assert queue._queue.qsize() == 1 @pytest.mark.asyncio async def test_enqueue_different_mbids(self, queue: AudioDBBrowseQueue): await queue.enqueue("artist", "abc-123") await queue.enqueue("album", "def-456") assert queue._queue.qsize() == 2 @pytest.mark.asyncio async def test_enqueue_full_queue_drops(self, queue: AudioDBBrowseQueue): queue._queue = asyncio.Queue(maxsize=2) await queue.enqueue("artist", "a") await queue.enqueue("artist", "b") await queue.enqueue("artist", "c") assert queue._queue.qsize() == 2 @pytest.mark.asyncio async def test_dedup_expires(self, queue: AudioDBBrowseQueue): await queue.enqueue("artist", "abc-123") queue._recent["abc-123"] -= 4000 await queue.enqueue("artist", "abc-123") assert queue._queue.qsize() == 2 class TestConsumer: @pytest.mark.asyncio async def test_consumer_processes_artist(self, queue, mock_audiodb_svc, mock_prefs): await queue.enqueue("artist", "abc-123", name="Coldplay") task = queue.start_consumer(mock_audiodb_svc, mock_prefs) await asyncio.sleep(0.05) task.cancel() try: await task except asyncio.CancelledError: pass mock_audiodb_svc.fetch_and_cache_artist_images.assert_called_once_with( "abc-123", "Coldplay", is_monitored=False, ) @pytest.mark.asyncio async def test_consumer_processes_album(self, queue, mock_audiodb_svc, mock_prefs): await queue.enqueue("album", "def-456", name="Parachutes", artist_name="Coldplay") task = queue.start_consumer(mock_audiodb_svc, mock_prefs) await asyncio.sleep(0.05) task.cancel() try: await task except asyncio.CancelledError: pass mock_audiodb_svc.fetch_and_cache_album_images.assert_called_once_with( "def-456", artist_name="Coldplay", album_name="Parachutes", is_monitored=False, ) @pytest.mark.asyncio async def test_consumer_skips_when_disabled(self, queue, mock_audiodb_svc, mock_prefs): mock_prefs.get_advanced_settings.return_value.audiodb_enabled = False await queue.enqueue("artist", "abc-123", name="Coldplay") task = queue.start_consumer(mock_audiodb_svc, mock_prefs) await asyncio.sleep(0.05) task.cancel() try: await task except asyncio.CancelledError: pass mock_audiodb_svc.fetch_and_cache_artist_images.assert_not_called() @pytest.mark.asyncio async def test_consumer_handles_item_error(self, queue, mock_audiodb_svc, mock_prefs, caplog): mock_audiodb_svc.fetch_and_cache_artist_images.side_effect = RuntimeError("boom") await queue.enqueue("artist", "abc-123", name="Coldplay") caplog.set_level(logging.ERROR, logger="services.audiodb_browse_queue") task = queue.start_consumer(mock_audiodb_svc, mock_prefs) await asyncio.sleep(0.05) task.cancel() try: await task except asyncio.CancelledError: pass assert queue._queue.qsize() == 0 assert any( record.levelno == logging.ERROR and "audiodb.browse_queue action=item_error" in record.message and "entity_type=artist" in record.message and "mbid=abc-123" in record.message for record in caplog.records )