From 50fe4bec97757646884b0459547c190849706dfd Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Mon, 4 May 2026 21:03:37 -0700 Subject: [PATCH] A4: Pin SoulSyncClient observable behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 5 tests pin the SoulSync standalone client surface — the structurally-different one (no auth, no API, no library scan). is_connected just checks os.path.isdir(transfer_path). ensure_connection reloads config first so the user changing the transfer_path takes effect without a process restart. get_all_album_ids returns a set of MD5-hashed string ids matching cross-server uniform set semantics. --- tests/media_server/test_soulsync_pinning.py | 90 +++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 tests/media_server/test_soulsync_pinning.py diff --git a/tests/media_server/test_soulsync_pinning.py b/tests/media_server/test_soulsync_pinning.py new file mode 100644 index 00000000..d419c829 --- /dev/null +++ b/tests/media_server/test_soulsync_pinning.py @@ -0,0 +1,90 @@ +"""Phase A pinning tests for SoulSyncClient (standalone library mode). + +Pin the OBSERVABLE BEHAVIOR the engine will dispatch through. +SoulSync standalone is the structurally-different one — no auth / +no API / no library scan. is_connected just checks `transfer_path` +is a directory. Filesystem walk happens via _get_cached_scan(). +""" + +from __future__ import annotations + +import os +from unittest.mock import patch + +import pytest + +from core.soulsync_client import SoulSyncClient + + +@pytest.fixture +def ss_client(tmp_path): + """A bare SoulSyncClient pointed at a temp directory.""" + client = SoulSyncClient.__new__(SoulSyncClient) + client._transfer_path = str(tmp_path) + client._progress_callback = None + client._cache = None + client._cache_time = 0 + client._cache_ttl = 300 + client._last_scan_time = None + return client + + +# --------------------------------------------------------------------------- +# is_connected / ensure_connection +# --------------------------------------------------------------------------- + + +def test_is_connected_true_when_transfer_path_exists(ss_client, tmp_path): + """Pinning: filesystem-based connectivity. is_connected just + checks os.path.isdir(transfer_path).""" + assert ss_client.is_connected() is True # tmp_path exists + + +def test_is_connected_false_when_transfer_path_missing(ss_client): + """Pinning: missing transfer_path → False. The standalone case + where the user hasn't configured a folder yet.""" + ss_client._transfer_path = '/nonexistent/path/that/does/not/exist' + assert ss_client.is_connected() is False + + +def test_ensure_connection_reloads_config_then_checks_path(ss_client, tmp_path): + """Pinning: ensure_connection re-reads config (so the user + changing the transfer_path takes effect without a process + restart) and then checks the path. Returns True iff the path + is a directory.""" + with patch.object(ss_client, '_reload_config') as fake_reload: + result = ss_client.ensure_connection() + + fake_reload.assert_called_once() + assert result is True + + +# --------------------------------------------------------------------------- +# get_all_artists / get_all_album_ids +# --------------------------------------------------------------------------- + + +def test_get_all_artists_uses_cached_scan(ss_client): + """Pinning: get_all_artists delegates to _get_cached_scan which + enforces the 5-min TTL. Engine extraction must preserve.""" + fake_artists = [object(), object(), object()] + with patch.object(ss_client, '_get_cached_scan', return_value=fake_artists): + result = ss_client.get_all_artists() + assert result == fake_artists + + +def test_get_all_album_ids_returns_set(ss_client): + """Pinning: returns a set of MD5-hashed string ids. Same uniform + set semantics as Plex/Jellyfin/Navidrome.""" + fake_album = type('FakeAlbum', (), {'ratingKey': 'hash-1'})() + fake_album2 = type('FakeAlbum', (), {'ratingKey': 'hash-2'})() + # Real SoulSyncArtist exposes albums() method (not _albums attr). + fake_artist = type('FakeArtist', (), { + 'albums': lambda self: [fake_album, fake_album2], + })() + + with patch.object(ss_client, '_get_cached_scan', return_value=[fake_artist]): + result = ss_client.get_all_album_ids() + + assert isinstance(result, set) + assert result == {'hash-1', 'hash-2'}