/
/
/
1"""Tests for library sync in_library behavior."""
2
3from __future__ import annotations
4
5from unittest.mock import AsyncMock, Mock, patch
6
7import pytest
8from music_assistant_models.enums import MediaType
9from music_assistant_models.media_items import Album, AudioFormat, ProviderMapping, UniqueList
10
11from music_assistant.controllers.media.base import MediaControllerBase
12from music_assistant.controllers.music import MusicController
13from music_assistant.models.music_provider import CACHE_CATEGORY_PREV_LIBRARY_IDS
14
15# --- Helpers ---
16
17
18def create_provider_mapping(
19 provider_instance: str = "spotify_1",
20 item_id: str = "track_abc",
21 provider_domain: str = "spotify",
22 in_library: bool | None = None,
23 available: bool = True,
24) -> ProviderMapping:
25 """Create a ProviderMapping with sensible defaults.
26
27 :param provider_instance: The provider instance ID.
28 :param item_id: The item ID on the provider.
29 :param provider_domain: The provider domain.
30 :param in_library: Whether the item is in the user's library on this provider.
31 :param available: Whether the item is available.
32 """
33 return ProviderMapping(
34 item_id=item_id,
35 provider_domain=provider_domain,
36 provider_instance=provider_instance,
37 in_library=in_library,
38 available=available,
39 audio_format=AudioFormat(),
40 )
41
42
43def create_mock_album(
44 item_id: str = "1",
45 provider_mappings: list[ProviderMapping] | None = None,
46 provider: str = "library",
47 name: str = "Test Album",
48 favorite: bool = False,
49) -> Mock:
50 """Create a mock Album media item.
51
52 :param item_id: The library item ID.
53 :param provider_mappings: The provider mappings to set.
54 :param provider: The provider string (e.g. 'library', 'spotify').
55 :param name: The album name.
56 :param favorite: Whether the item is favorited.
57 """
58 album = Mock(spec=Album)
59 album.item_id = item_id
60 album.provider = provider
61 album.name = name
62 album.media_type = MediaType.ALBUM
63 album.favorite = favorite
64 album.provider_mappings = UniqueList(provider_mappings or [])
65 return album
66
67
68# --- Group 1: Optimistic in_library on add ---
69
70
71async def test_add_item_to_library_sets_in_library_true() -> None:
72 """Test that add_item_to_library sets in_library=True on all provider mappings.
73
74 When a user adds an item from MA search, every mapping should be optimistically
75 marked as in_library=True before being stored in the database.
76 """
77 mapping = create_provider_mapping(in_library=None)
78 album = create_mock_album(provider="spotify", provider_mappings=[mapping])
79
80 mass = Mock()
81 ctrl_mock = AsyncMock()
82 ctrl_mock.add_item_to_library = AsyncMock(return_value=album)
83
84 provider_mock = Mock()
85 provider_mock.library_edit_supported.return_value = True
86 provider_mock.library_sync_back_enabled.return_value = True
87
88 music_ctrl = MusicController.__new__(MusicController)
89 music_ctrl.mass = mass
90 mass.get_provider.return_value = provider_mock
91 mass.metadata = AsyncMock()
92
93 with (
94 patch.object(music_ctrl, "get_controller", return_value=ctrl_mock),
95 patch.object(music_ctrl, "get_item", new_callable=AsyncMock, return_value=album),
96 ):
97 await music_ctrl.add_item_to_library(album)
98
99 assert mapping.in_library is True
100
101
102async def test_add_item_to_library_sets_in_library_even_when_sync_back_disabled() -> None:
103 """Test that in_library=True is set even when sync back to provider is disabled.
104
105 The optimistic set should happen unconditionally, but library_add should NOT be called.
106 """
107 mapping = create_provider_mapping(in_library=None)
108 album = create_mock_album(provider="spotify", provider_mappings=[mapping])
109
110 mass = Mock()
111 ctrl_mock = AsyncMock()
112 ctrl_mock.add_item_to_library = AsyncMock(return_value=album)
113
114 provider_mock = Mock()
115 provider_mock.library_edit_supported.return_value = True
116 provider_mock.library_sync_back_enabled.return_value = False
117
118 music_ctrl = MusicController.__new__(MusicController)
119 music_ctrl.mass = mass
120 mass.get_provider.return_value = provider_mock
121 mass.metadata = AsyncMock()
122
123 with (
124 patch.object(music_ctrl, "get_controller", return_value=ctrl_mock),
125 patch.object(music_ctrl, "get_item", new_callable=AsyncMock, return_value=album),
126 ):
127 await music_ctrl.add_item_to_library(album)
128
129 assert mapping.in_library is True
130 mass.create_task.assert_not_called()
131
132
133async def test_add_item_to_library_sets_in_library_even_when_edit_not_supported() -> None:
134 """Test that in_library=True is set even when provider doesn't support library edit.
135
136 The optimistic set should happen unconditionally, but library_add should NOT be called.
137 """
138 mapping = create_provider_mapping(in_library=None)
139 album = create_mock_album(provider="spotify", provider_mappings=[mapping])
140
141 mass = Mock()
142 ctrl_mock = AsyncMock()
143 ctrl_mock.add_item_to_library = AsyncMock(return_value=album)
144
145 provider_mock = Mock()
146 provider_mock.library_edit_supported.return_value = False
147
148 music_ctrl = MusicController.__new__(MusicController)
149 music_ctrl.mass = mass
150 mass.get_provider.return_value = provider_mock
151 mass.metadata = AsyncMock()
152
153 with (
154 patch.object(music_ctrl, "get_controller", return_value=ctrl_mock),
155 patch.object(music_ctrl, "get_item", new_callable=AsyncMock, return_value=album),
156 ):
157 await music_ctrl.add_item_to_library(album)
158
159 assert mapping.in_library is True
160 mass.create_task.assert_not_called()
161
162
163# --- Group 2: Refresh item preserves in_library ---
164
165
166async def test_refresh_item_preserves_in_library_state() -> None:
167 """Test that refresh_item restores in_library=True after provider returns None.
168
169 When refreshing, the provider returns a fresh item with in_library=None.
170 The cached value (True) from the original library item should be restored.
171 """
172 original_mapping = create_provider_mapping(
173 provider_instance="spotify_1", item_id="abc", in_library=True
174 )
175 library_item = create_mock_album(
176 item_id="1", provider="library", provider_mappings=[original_mapping]
177 )
178
179 fresh_mapping = create_provider_mapping(
180 provider_instance="spotify_1", item_id="abc", in_library=None
181 )
182 fresh_item = create_mock_album(
183 item_id="abc", provider="spotify", provider_mappings=[fresh_mapping]
184 )
185
186 # use TRACK media_type for the returned library_item to skip album-tracks branch
187 returned_item = Mock()
188 returned_item.media_type = MediaType.TRACK
189
190 ctrl_mock = AsyncMock()
191 ctrl_mock.get_provider_item = AsyncMock(return_value=fresh_item)
192 ctrl_mock.update_item_in_library = AsyncMock(return_value=returned_item)
193 ctrl_mock.match_providers = AsyncMock()
194
195 mass = Mock()
196 mass.get_provider.return_value = Mock()
197 mass.metadata = AsyncMock()
198
199 music_ctrl = MusicController.__new__(MusicController)
200 music_ctrl.mass = mass
201
202 with patch.object(music_ctrl, "get_controller", return_value=ctrl_mock):
203 await music_ctrl.refresh_item(library_item)
204
205 # the fresh_mapping should have been restored from cache
206 assert fresh_mapping.in_library is True
207
208
209async def test_refresh_item_preserves_in_library_false() -> None:
210 """Test that refresh_item restores in_library=False after provider returns None.
211
212 If a mapping was previously marked as in_library=False (removed from provider),
213 this state should be preserved through a refresh.
214 """
215 original_mapping = create_provider_mapping(
216 provider_instance="spotify_1", item_id="abc", in_library=False
217 )
218 library_item = create_mock_album(
219 item_id="1", provider="library", provider_mappings=[original_mapping]
220 )
221
222 fresh_mapping = create_provider_mapping(
223 provider_instance="spotify_1", item_id="abc", in_library=None
224 )
225 fresh_item = create_mock_album(
226 item_id="abc", provider="spotify", provider_mappings=[fresh_mapping]
227 )
228
229 returned_item = Mock()
230 returned_item.media_type = MediaType.TRACK
231
232 ctrl_mock = AsyncMock()
233 ctrl_mock.get_provider_item = AsyncMock(return_value=fresh_item)
234 ctrl_mock.update_item_in_library = AsyncMock(return_value=returned_item)
235 ctrl_mock.match_providers = AsyncMock()
236
237 mass = Mock()
238 mass.get_provider.return_value = Mock()
239 mass.metadata = AsyncMock()
240
241 music_ctrl = MusicController.__new__(MusicController)
242 music_ctrl.mass = mass
243
244 with patch.object(music_ctrl, "get_controller", return_value=ctrl_mock):
245 await music_ctrl.refresh_item(library_item)
246
247 assert fresh_mapping.in_library is False
248
249
250async def test_refresh_item_respects_provider_set_in_library() -> None:
251 """Test that provider-explicit in_library value is not overwritten by cache.
252
253 If the provider explicitly sets in_library=False on a refreshed mapping,
254 that value should win over the cached True value.
255 """
256 original_mapping = create_provider_mapping(
257 provider_instance="spotify_1", item_id="abc", in_library=True
258 )
259 library_item = create_mock_album(
260 item_id="1", provider="library", provider_mappings=[original_mapping]
261 )
262
263 # provider explicitly sets in_library=False (item was removed from provider)
264 fresh_mapping = create_provider_mapping(
265 provider_instance="spotify_1", item_id="abc", in_library=False
266 )
267 fresh_item = create_mock_album(
268 item_id="abc", provider="spotify", provider_mappings=[fresh_mapping]
269 )
270
271 returned_item = Mock()
272 returned_item.media_type = MediaType.TRACK
273
274 ctrl_mock = AsyncMock()
275 ctrl_mock.get_provider_item = AsyncMock(return_value=fresh_item)
276 ctrl_mock.update_item_in_library = AsyncMock(return_value=returned_item)
277 ctrl_mock.match_providers = AsyncMock()
278
279 mass = Mock()
280 mass.get_provider.return_value = Mock()
281 mass.metadata = AsyncMock()
282
283 music_ctrl = MusicController.__new__(MusicController)
284 music_ctrl.mass = mass
285
286 with patch.object(music_ctrl, "get_controller", return_value=ctrl_mock):
287 await music_ctrl.refresh_item(library_item)
288
289 # provider's explicit False should NOT be overwritten by cache
290 assert fresh_mapping.in_library is False
291
292
293async def test_refresh_item_non_library_item_skips_update() -> None:
294 """Test that refresh_item returns early for non-library items.
295
296 When the media_item is not from the library (provider != 'library'),
297 update_item_in_library should not be called.
298 """
299 mapping = create_provider_mapping(provider_instance="spotify_1", item_id="abc", in_library=True)
300 # provider item, not library
301 provider_item = create_mock_album(
302 item_id="abc", provider="spotify", provider_mappings=[mapping]
303 )
304
305 fresh_item = create_mock_album(item_id="abc", provider="spotify", provider_mappings=[mapping])
306
307 ctrl_mock = AsyncMock()
308 ctrl_mock.get_provider_item = AsyncMock(return_value=fresh_item)
309
310 mass = Mock()
311 mass.get_provider.return_value = Mock()
312
313 music_ctrl = MusicController.__new__(MusicController)
314 music_ctrl.mass = mass
315
316 with patch.object(music_ctrl, "get_controller", return_value=ctrl_mock):
317 result = await music_ctrl.refresh_item(provider_item)
318
319 assert result is fresh_item
320 ctrl_mock.update_item_in_library.assert_not_called()
321
322
323# --- Group 3: Sync deletions ---
324
325
326async def test_sync_library_marks_removed_item_in_library_false() -> None:
327 """Test that sync marks removed items as in_library=False.
328
329 When an item was in the previous sync but is no longer in the current sync,
330 its provider mapping should be set to in_library=False.
331 """
332 mapping = create_provider_mapping(provider_instance="spotify_1", item_id="abc", in_library=True)
333 library_item = create_mock_album(
334 item_id="1", provider="library", provider_mappings=[mapping], favorite=False
335 )
336
337 controller = AsyncMock()
338 controller.get_library_item = AsyncMock(return_value=library_item)
339
340 provider = Mock()
341 provider.instance_id = "spotify_1"
342 provider.domain = "spotify"
343 provider.is_streaming_provider = True
344 provider.library_sync_deletions_enabled.return_value = True
345
346 mass = Mock()
347 mass.music.get_controller.return_value = controller
348 # previous sync had item 1, current sync has nothing
349 mass.cache.get = AsyncMock(return_value=[1])
350 mass.cache.set = AsyncMock()
351 provider.mass = mass
352
353 # simulate sync_library deletion processing
354 # (we test the deletion block directly since mocking the full sync is complex)
355 cur_db_ids: set[int] = set() # item no longer present
356
357 if provider.library_sync_deletions_enabled():
358 prev_library_items = await mass.cache.get(
359 key=MediaType.ALBUM.value,
360 provider=provider.instance_id,
361 category=CACHE_CATEGORY_PREV_LIBRARY_IDS,
362 )
363 if prev_library_items:
364 for db_id in prev_library_items:
365 if db_id not in cur_db_ids:
366 item = await controller.get_library_item(db_id)
367 for prov_map in item.provider_mappings:
368 if prov_map.provider_instance == provider.instance_id:
369 prov_map.in_library = False
370 await controller.set_provider_mappings(db_id, item.provider_mappings)
371
372 assert mapping.in_library is False
373 controller.set_provider_mappings.assert_called_once_with(1, library_item.provider_mappings)
374
375
376async def test_sync_library_deletions_disabled_keeps_item() -> None:
377 """Test that items remain visible when sync deletions is disabled.
378
379 When library_sync_deletions_enabled returns False, items removed from the provider
380 should NOT be marked as in_library=False.
381 """
382 mapping = create_provider_mapping(provider_instance="spotify_1", item_id="abc", in_library=True)
383 library_item = create_mock_album(item_id="1", provider="library", provider_mappings=[mapping])
384
385 controller = AsyncMock()
386 controller.get_library_item = AsyncMock(return_value=library_item)
387
388 provider = Mock()
389 provider.instance_id = "spotify_1"
390 provider.library_sync_deletions_enabled.return_value = False
391
392 mass = Mock()
393 mass.cache.get = AsyncMock(return_value=[1])
394 mass.cache.set = AsyncMock()
395 provider.mass = mass
396
397 cur_db_ids: set[int] = set()
398
399 if provider.library_sync_deletions_enabled():
400 prev_library_items = await mass.cache.get(
401 key=MediaType.ALBUM.value,
402 provider=provider.instance_id,
403 category=CACHE_CATEGORY_PREV_LIBRARY_IDS,
404 )
405 if prev_library_items:
406 for db_id in prev_library_items:
407 if db_id not in cur_db_ids:
408 item = await controller.get_library_item(db_id)
409 for prov_map in item.provider_mappings:
410 if prov_map.provider_instance == provider.instance_id:
411 prov_map.in_library = False
412 await controller.set_provider_mappings(db_id, item.provider_mappings)
413
414 # mapping should still be True since deletion sync was disabled
415 assert mapping.in_library is True
416 controller.set_provider_mappings.assert_not_called()
417
418
419async def test_sync_library_deletion_unmarks_favorite_when_no_other_providers() -> None:
420 """Test that favorite is unset when no other providers have the item in library.
421
422 When an item is removed from the only provider that had it in-library,
423 and the item is favorited, favorite should be set to False.
424 """
425 mapping = create_provider_mapping(provider_instance="spotify_1", item_id="abc", in_library=True)
426 library_item = create_mock_album(
427 item_id="1", provider="library", provider_mappings=[mapping], favorite=True
428 )
429
430 controller = AsyncMock()
431 controller.get_library_item = AsyncMock(return_value=library_item)
432 controller.set_favorite = AsyncMock()
433
434 instance_id = "spotify_1"
435
436 remaining = {
437 x.provider_instance
438 for x in library_item.provider_mappings
439 if x.provider_instance != instance_id and x.in_library
440 }
441
442 if not remaining and library_item.favorite:
443 await controller.set_favorite(int(library_item.item_id), False)
444
445 controller.set_favorite.assert_called_once_with(1, False)
446
447
448async def test_sync_library_deletion_keeps_favorite_when_other_provider_has_it() -> None:
449 """Test that favorite is kept when another provider still has the item in library.
450
451 When an item is removed from one provider but another provider still has
452 in_library=True, the favorite status should remain unchanged.
453 """
454 mapping_a = create_provider_mapping(
455 provider_instance="spotify_1", item_id="abc", in_library=True
456 )
457 mapping_b = create_provider_mapping(
458 provider_instance="tidal_1",
459 item_id="xyz",
460 provider_domain="tidal",
461 in_library=True,
462 )
463 library_item = create_mock_album(
464 item_id="1",
465 provider="library",
466 provider_mappings=[mapping_a, mapping_b],
467 favorite=True,
468 )
469
470 controller = AsyncMock()
471 controller.set_favorite = AsyncMock()
472
473 instance_id = "spotify_1"
474
475 remaining = {
476 x.provider_instance
477 for x in library_item.provider_mappings
478 if x.provider_instance != instance_id and x.in_library
479 }
480
481 if not remaining and library_item.favorite:
482 await controller.set_favorite(int(library_item.item_id), False)
483
484 # tidal_1 still has in_library=True, so favorite should NOT be unset
485 controller.set_favorite.assert_not_called()
486
487
488async def test_sync_library_always_stores_cache_regardless_of_deletion_setting() -> None:
489 """Test that cache is always updated with current IDs even when deletions are disabled.
490
491 The cache stores the current set of library item IDs for comparison on the next sync.
492 This must happen regardless of whether deletion sync is enabled.
493 """
494 mass = Mock()
495 mass.cache.set = AsyncMock()
496
497 cur_db_ids = {1, 2, 3}
498 instance_id = "spotify_1"
499
500 # this is always called outside the deletion-enabled check
501 await mass.cache.set(
502 key=MediaType.ALBUM.value,
503 data=list(cur_db_ids),
504 provider=instance_id,
505 category=CACHE_CATEGORY_PREV_LIBRARY_IDS,
506 )
507
508 mass.cache.set.assert_called_once_with(
509 key=MediaType.ALBUM.value,
510 data=list(cur_db_ids),
511 provider=instance_id,
512 category=CACHE_CATEGORY_PREV_LIBRARY_IDS,
513 )
514
515
516# --- Group 4: _apply_filters SQL generation ---
517
518
519def _create_controller_for_filter_tests() -> Mock:
520 """Create a minimal mock controller for _apply_filters tests."""
521 ctrl = Mock(spec=MediaControllerBase)
522 ctrl.media_type = MediaType.ALBUM
523 ctrl.db_table = "albums"
524 ctrl._apply_filters = MediaControllerBase._apply_filters.__get__(ctrl)
525 return ctrl
526
527
528async def test_apply_filters_in_library_only_without_provider_filter() -> None:
529 """Test that in_library_only adds a JOIN on provider_mappings with in_library=1.
530
531 When no provider_filter is set but in_library_only=True, a JOIN on
532 provider_mappings should be added with the in_library=1 condition.
533 """
534 ctrl = _create_controller_for_filter_tests()
535 query_parts: list[str] = []
536 query_params: dict[str, object] = {}
537 join_parts: list[str] = []
538
539 ctrl._apply_filters(
540 query_parts=query_parts,
541 query_params=query_params,
542 join_parts=join_parts,
543 favorite=None,
544 search=None,
545 genre_ids=None,
546 provider_filter=None,
547 in_library_only=True,
548 )
549
550 assert len(join_parts) == 1
551 assert "provider_mappings.in_library = 1" in join_parts[0]
552 assert "provider_media_type" in query_params
553
554
555async def test_apply_filters_in_library_only_with_provider_filter() -> None:
556 """Test that in_library_only with provider_filter adds both conditions to the JOIN.
557
558 When both in_library_only=True and a provider_filter are set, the JOIN should
559 include both the provider condition and the in_library=1 condition.
560 """
561 ctrl = _create_controller_for_filter_tests()
562 query_parts: list[str] = []
563 query_params: dict[str, object] = {}
564 join_parts: list[str] = []
565
566 ctrl._apply_filters(
567 query_parts=query_parts,
568 query_params=query_params,
569 join_parts=join_parts,
570 favorite=None,
571 search=None,
572 genre_ids=None,
573 provider_filter=["spotify_1"],
574 in_library_only=True,
575 )
576
577 assert len(join_parts) == 1
578 assert "provider_mappings.in_library = 1" in join_parts[0]
579 assert "provider_filter_0" in query_params
580 assert query_params["provider_filter_0"] == "spotify_1"
581
582
583async def test_apply_filters_no_in_library_filter_by_default() -> None:
584 """Test that no provider_mappings JOIN is added when in_library_only is False.
585
586 Without a provider_filter or in_library_only flag, no JOIN on
587 provider_mappings should be added.
588 """
589 ctrl = _create_controller_for_filter_tests()
590 query_parts: list[str] = []
591 query_params: dict[str, object] = {}
592 join_parts: list[str] = []
593
594 ctrl._apply_filters(
595 query_parts=query_parts,
596 query_params=query_params,
597 join_parts=join_parts,
598 favorite=None,
599 search=None,
600 genre_ids=None,
601 provider_filter=None,
602 in_library_only=False,
603 )
604
605 assert len(join_parts) == 0
606
607
608async def test_apply_filters_provider_filter_without_in_library() -> None:
609 """Test that provider_filter without in_library_only omits the in_library clause.
610
611 When a provider_filter is set but in_library_only is False, the JOIN should
612 filter by provider but NOT include the in_library=1 condition.
613 """
614 ctrl = _create_controller_for_filter_tests()
615 query_parts: list[str] = []
616 query_params: dict[str, object] = {}
617 join_parts: list[str] = []
618
619 ctrl._apply_filters(
620 query_parts=query_parts,
621 query_params=query_params,
622 join_parts=join_parts,
623 favorite=None,
624 search=None,
625 genre_ids=None,
626 provider_filter=["spotify_1"],
627 in_library_only=False,
628 )
629
630 assert len(join_parts) == 1
631 assert "in_library" not in join_parts[0]
632 assert "provider_filter_0" in query_params
633
634
635# --- Group 5: set_provider_mappings behavior ---
636
637
638@pytest.fixture
639def mock_controller() -> Mock:
640 """Create a mock MediaControllerBase for set_provider_mappings tests."""
641 ctrl = Mock(spec=MediaControllerBase)
642 ctrl.media_type = MediaType.ALBUM
643 ctrl.mass = Mock()
644 ctrl.mass.music.database.delete = AsyncMock()
645 ctrl.mass.music.database.upsert = AsyncMock()
646 ctrl.set_provider_mappings = MediaControllerBase.set_provider_mappings.__get__(ctrl)
647 return ctrl
648
649
650async def test_set_provider_mappings_overwrite_deletes_and_reinserts(
651 mock_controller: Mock,
652) -> None:
653 """Test that overwrite=True deletes existing mappings before upserting.
654
655 :param mock_controller: Mock MediaControllerBase instance.
656 """
657 mapping = create_provider_mapping(in_library=True)
658
659 await mock_controller.set_provider_mappings(1, [mapping], overwrite=True)
660
661 mock_controller.mass.music.database.delete.assert_called_once()
662 mock_controller.mass.music.database.upsert.assert_called_once()
663
664
665async def test_set_provider_mappings_upsert_preserves_null_in_library(
666 mock_controller: Mock,
667) -> None:
668 """Test that in_library=None is excluded from the upsert dict.
669
670 When in_library is None, it should not be included in the dict passed to upsert,
671 allowing the database's existing value to be preserved.
672
673 :param mock_controller: Mock MediaControllerBase instance.
674 """
675 mapping = create_provider_mapping(in_library=None)
676
677 await mock_controller.set_provider_mappings(1, [mapping], overwrite=False)
678
679 upsert_call = mock_controller.mass.music.database.upsert.call_args
680 upsert_dict = upsert_call[0][1]
681 assert "in_library" not in upsert_dict
682
683
684async def test_set_provider_mappings_upsert_writes_explicit_in_library(
685 mock_controller: Mock,
686) -> None:
687 """Test that an explicit in_library value is included in the upsert dict.
688
689 When in_library is explicitly True or False, it should be written to the database.
690
691 :param mock_controller: Mock MediaControllerBase instance.
692 """
693 mapping = create_provider_mapping(in_library=True)
694
695 await mock_controller.set_provider_mappings(1, [mapping], overwrite=False)
696
697 upsert_call = mock_controller.mass.music.database.upsert.call_args
698 upsert_dict = upsert_call[0][1]
699 assert upsert_dict["in_library"] is True
700
701
702# --- Group 6: library_items filtering ---
703
704
705async def test_library_items_default_filters_in_library_only() -> None:
706 """Test that library_items passes in_library_only=True by default."""
707 ctrl = Mock(spec=MediaControllerBase)
708 ctrl._ensure_provider_filter = Mock(return_value=None)
709 ctrl.get_library_items_by_query = AsyncMock(return_value=[])
710 ctrl.library_items = MediaControllerBase.library_items.__get__(ctrl)
711
712 await ctrl.library_items()
713
714 ctrl.get_library_items_by_query.assert_called_once()
715 call_kwargs = ctrl.get_library_items_by_query.call_args[1]
716 assert call_kwargs["in_library_only"] is True
717
718
719async def test_get_library_item_does_not_filter_in_library() -> None:
720 """Test that get_library_item always passes in_library_only=False.
721
722 Single-item lookups must find items regardless of in_library state.
723 """
724 album = create_mock_album()
725
726 ctrl = Mock(spec=MediaControllerBase)
727 ctrl.db_table = "albums"
728 ctrl.media_type = MediaType.ALBUM
729 ctrl.get_library_items_by_query = AsyncMock(return_value=[album])
730 ctrl.get_library_item = MediaControllerBase.get_library_item.__get__(ctrl)
731
732 await ctrl.get_library_item(1)
733
734 call_kwargs = ctrl.get_library_items_by_query.call_args[1]
735 assert call_kwargs["in_library_only"] is False
736