mirror of https://github.com/Nezreka/SoulSync.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
193 lines
8.2 KiB
193 lines
8.2 KiB
"""Pin Plex scan-trigger + scan-status methods against non-English
|
|
section names — issue #535.
|
|
|
|
Background
|
|
----------
|
|
|
|
A Plex server with the music library named "Música" (Spanish),
|
|
"Musique" (French), "Musik" (German), etc. — type still
|
|
``artist`` — would have ``_find_music_library`` correctly
|
|
auto-detect the section by type. ``self.music_library`` was
|
|
populated correctly. Read methods (``get_artists`` etc.) routed
|
|
through ``_get_music_sections`` which returned ``[self.music_library]``
|
|
and worked.
|
|
|
|
But ``trigger_library_scan`` and ``is_library_scanning`` ignored
|
|
``self.music_library`` and called ``self.server.library.section(library_name)``
|
|
with a hardcoded ``"Music"`` default. ``server.library.section('Music')``
|
|
raised ``NotFound`` for any server whose music section wasn't
|
|
literally named "Music", so post-import scans never fired.
|
|
|
|
Side effect: wishlist.processing kept reporting "Missing from
|
|
media server after sync" for tracks that DID import correctly,
|
|
re-adding them to the wishlist forever.
|
|
|
|
These tests pin both methods through the auto-detected-section
|
|
path. Both single-library + all-libraries modes get coverage.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from core.plex_client import PlexClient
|
|
|
|
|
|
def _make_client(*, all_libraries_mode: bool = False, music_library=None, server=None):
|
|
"""Same fixture pattern as test_plex_all_libraries.py — minimal
|
|
construction skipping `__init__` so the test owns the entire
|
|
client state."""
|
|
client = PlexClient.__new__(PlexClient)
|
|
client.server = server
|
|
client.music_library = music_library
|
|
client._all_libraries_mode = all_libraries_mode
|
|
client._connection_attempted = server is not None
|
|
client._is_connecting = False
|
|
client._last_connection_check = 0
|
|
client._connection_check_interval = 30
|
|
return client
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# trigger_library_scan — single-library mode (the broken path in #535)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestTriggerLibraryScanNonEnglishSection:
|
|
|
|
@pytest.mark.parametrize('section_name', ['Música', 'Musique', 'Musik', 'Musica', '音乐', 'موسيقى'])
|
|
def test_uses_auto_detected_section_regardless_of_locale(self, section_name):
|
|
"""The fix's headline assertion. Auto-detected section's
|
|
update() must be called — NOT a literal 'Music' lookup that
|
|
would NotFound on any non-English server."""
|
|
section = MagicMock(title=section_name)
|
|
server = MagicMock()
|
|
# If the code falls back to literal lookup, raise so the test
|
|
# fails loudly instead of silently calling the wrong method.
|
|
server.library.section.side_effect = AssertionError(
|
|
f"Should NOT call server.library.section() when "
|
|
f"music_library is populated with '{section_name}'"
|
|
)
|
|
client = _make_client(server=server, music_library=section)
|
|
|
|
result = client.trigger_library_scan()
|
|
|
|
assert result is True
|
|
section.update.assert_called_once()
|
|
|
|
def test_falls_back_to_literal_lookup_when_no_auto_detection(self):
|
|
"""Backward compat: if music_library is None (test fixtures,
|
|
edge cases where auto-detection hasn't run), fall back to the
|
|
literal `library_name` lookup as before. Default 'Music' arg
|
|
preserved — calling with no kwargs still works."""
|
|
looked_up = MagicMock(title='Music')
|
|
server = MagicMock()
|
|
server.library.section.return_value = looked_up
|
|
client = _make_client(server=server, music_library=None)
|
|
|
|
result = client.trigger_library_scan()
|
|
|
|
assert result is True
|
|
server.library.section.assert_called_once_with('Music')
|
|
looked_up.update.assert_called_once()
|
|
|
|
def test_explicit_library_name_arg_used_only_when_no_auto_detection(self):
|
|
"""When music_library is populated, the library_name kwarg is
|
|
ignored — auto-detected section wins. Otherwise the kwarg
|
|
controls the literal lookup."""
|
|
section = MagicMock(title='Música')
|
|
server = MagicMock()
|
|
server.library.section.side_effect = AssertionError("must not fall back")
|
|
client = _make_client(server=server, music_library=section)
|
|
|
|
# Pass a non-default library_name — auto-detected wins.
|
|
result = client.trigger_library_scan(library_name='Whatever')
|
|
|
|
assert result is True
|
|
section.update.assert_called_once()
|
|
|
|
def test_logs_correct_section_label_on_success(self, caplog):
|
|
"""Log line must surface the actual section's title (`Música`)
|
|
not the unused library_name default ('Music'). Pre-fix the
|
|
success log read 'Triggered Plex library scan for Music' even
|
|
on Spanish servers — confusing when debugging."""
|
|
section = MagicMock(title='Música')
|
|
client = _make_client(server=MagicMock(), music_library=section)
|
|
|
|
with caplog.at_level('INFO', logger='soulsync.plex_client'):
|
|
client.trigger_library_scan()
|
|
|
|
assert any('Música' in r.getMessage() for r in caplog.records), (
|
|
f"Expected log to mention 'Música'; got "
|
|
f"{[r.getMessage() for r in caplog.records]}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# is_library_scanning — symmetric fix
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestIsLibraryScanningNonEnglishSection:
|
|
|
|
def test_uses_auto_detected_section_for_refreshing_check(self):
|
|
"""`is_library_scanning` must check the auto-detected
|
|
section's `refreshing` attribute — NOT a literal-name
|
|
lookup that fails on non-English servers."""
|
|
section = MagicMock(title='Música', refreshing=True)
|
|
server = MagicMock()
|
|
server.activities.return_value = []
|
|
server.library.section.side_effect = AssertionError("must not fall back")
|
|
client = _make_client(server=server, music_library=section)
|
|
|
|
result = client.is_library_scanning()
|
|
|
|
assert result is True
|
|
|
|
def test_activity_match_uses_resolved_section_title(self):
|
|
"""Activity feed match must filter by the resolved section's
|
|
title — NOT the library_name kwarg default ('Music'). Pre-fix
|
|
a Spanish server's "Música" scan activity wouldn't match the
|
|
literal 'music' substring check (well, "música".contains("music")
|
|
IS true here by coincidence — but for "Musique" / "Musik" /
|
|
"音乐" / "موسيقى" the substring miss is real)."""
|
|
section = MagicMock(title='موسيقى', refreshing=False)
|
|
activity = MagicMock(type='library.scan', title='Scanning موسيقى')
|
|
server = MagicMock()
|
|
server.activities.return_value = [activity]
|
|
server.library.section.side_effect = AssertionError("must not fall back")
|
|
client = _make_client(server=server, music_library=section)
|
|
|
|
result = client.is_library_scanning()
|
|
|
|
assert result is True
|
|
|
|
def test_no_match_when_activity_for_unrelated_section(self):
|
|
"""Sanity: activity for a DIFFERENT section's scan shouldn't
|
|
cause a false-positive "music library is scanning" reading."""
|
|
section = MagicMock(title='Música', refreshing=False)
|
|
# Activity is for a different section (e.g. Movies)
|
|
activity = MagicMock(type='library.scan', title='Scanning Películas')
|
|
server = MagicMock()
|
|
server.activities.return_value = [activity]
|
|
server.library.section.side_effect = AssertionError("must not fall back")
|
|
client = _make_client(server=server, music_library=section)
|
|
|
|
assert client.is_library_scanning() is False
|
|
|
|
def test_falls_back_to_literal_lookup_when_no_auto_detection(self):
|
|
"""Backward compat: music_library=None → literal section
|
|
lookup by library_name (default 'Music'). Same as before fix."""
|
|
looked_up = MagicMock(title='Music', refreshing=False)
|
|
server = MagicMock()
|
|
server.library.section.return_value = looked_up
|
|
server.activities.return_value = []
|
|
client = _make_client(server=server, music_library=None)
|
|
|
|
result = client.is_library_scanning()
|
|
|
|
assert result is False
|
|
server.library.section.assert_called_once_with('Music')
|