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