/
/
/
1"""Tests for LocalFileSystemProvider cover image cache-busting."""
2
3from unittest.mock import AsyncMock, MagicMock, patch
4
5import pytest
6from music_assistant_models.errors import MediaNotFoundError
7
8from music_assistant.providers.filesystem_local import LocalFileSystemProvider
9
10
11def _create_provider() -> LocalFileSystemProvider:
12 """Create a bare LocalFileSystemProvider without running __init__."""
13 with patch.object(LocalFileSystemProvider, "__init__", lambda *_a, **_kw: None):
14 provider = LocalFileSystemProvider.__new__(LocalFileSystemProvider)
15 provider.logger = MagicMock()
16 return provider
17
18
19class TestVersionedImagePath:
20 """Test the cover image path versioning and its round-trip in resolve_image."""
21
22 def test_appends_checksum(self) -> None:
23 """A checksum is appended as a query suffix."""
24 result = LocalFileSystemProvider._versioned_image_path("Moby Dick/folder.jpg", "123")
25 assert result == "Moby Dick/folder.jpg?cs=123"
26
27 def test_changes_when_file_replaced(self) -> None:
28 """A new checksum (file replaced in place) yields a different path."""
29 old = LocalFileSystemProvider._versioned_image_path("Moby Dick/folder.jpg", "123")
30 new = LocalFileSystemProvider._versioned_image_path("Moby Dick/folder.jpg", "456")
31 assert old != new
32
33 def test_no_checksum_is_unchanged(self) -> None:
34 """Without a checksum the path is returned untouched."""
35 assert (
36 LocalFileSystemProvider._versioned_image_path("Moby Dick/folder.jpg", None)
37 == "Moby Dick/folder.jpg"
38 )
39
40 @pytest.mark.asyncio
41 async def test_resolve_image_strips_suffix(self) -> None:
42 """resolve_image strips the cache-busting suffix before resolving the file."""
43 provider = _create_provider()
44 versioned = LocalFileSystemProvider._versioned_image_path("Moby Dick/folder.jpg", "123")
45 with patch.object(provider, "resolve", new_callable=AsyncMock) as mock_resolve:
46 mock_resolve.return_value = MagicMock(absolute_path="/music/folder.jpg")
47 await provider.resolve_image(versioned)
48 mock_resolve.assert_awaited_once_with("Moby Dick/folder.jpg")
49
50 @pytest.mark.asyncio
51 async def test_resolve_image_without_suffix(self) -> None:
52 """A plain path without a suffix is resolved unchanged."""
53 provider = _create_provider()
54 with patch.object(provider, "resolve", new_callable=AsyncMock) as mock_resolve:
55 mock_resolve.return_value = MagicMock(absolute_path="/music/track.flac")
56 await provider.resolve_image("Moby Dick/track.flac")
57 mock_resolve.assert_awaited_once_with("Moby Dick/track.flac")
58
59 @pytest.mark.asyncio
60 async def test_resolve_image_missing_file_raises_media_not_found(self) -> None:
61 """A removed image file surfaces as a typed MediaNotFoundError with chaining."""
62 provider = _create_provider()
63 with patch.object(provider, "resolve", new_callable=AsyncMock) as mock_resolve:
64 mock_resolve.side_effect = FileNotFoundError
65 with pytest.raises(MediaNotFoundError) as exc_info:
66 await provider.resolve_image("Moby Dick/folder.jpg?cs=123")
67 assert isinstance(exc_info.value.__cause__, FileNotFoundError)
68