Snapshot of the existing codebase before working through the TASKS.md backlog. Real library data (data/) and the iTunes import fixture (itunes-test-library/) are gitignored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
106 lines
3.6 KiB
Python
106 lines
3.6 KiB
Python
import hashlib
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from lintunes.lastfm import sign, LastFm
|
|
from lintunes.player import make_shuffle_order
|
|
from lintunes.preferences import Preferences
|
|
|
|
|
|
class TestLastFmSignature:
|
|
def test_signature_matches_hand_computed(self):
|
|
params = {
|
|
"method": "auth.getMobileSession",
|
|
"api_key": "KEY",
|
|
"username": "trav",
|
|
"password": "hunter2",
|
|
"format": "json", # must be excluded from the signature
|
|
}
|
|
expected = hashlib.md5(
|
|
("api_keyKEY"
|
|
"methodauth.getMobileSession"
|
|
"passwordhunter2"
|
|
"usernametrav"
|
|
"SECRET").encode()).hexdigest()
|
|
assert sign(params, "SECRET") == expected
|
|
|
|
def test_format_and_callback_excluded(self):
|
|
base = {"method": "x", "api_key": "k"}
|
|
with_extras = dict(base, format="json", callback="cb")
|
|
assert sign(base, "s") == sign(with_extras, "s")
|
|
|
|
|
|
class TestScrobbleQueue:
|
|
def test_queue_round_trip(self, qapp, tmp_path):
|
|
prefs = Preferences(tmp_path)
|
|
lastfm = LastFm(prefs, tmp_path)
|
|
entry = {"artist": "A", "track": "T", "album": "L",
|
|
"timestamp": 123, "duration": 180}
|
|
lastfm._enqueue(entry)
|
|
assert lastfm._load_queue() == [entry]
|
|
lastfm._save_queue([])
|
|
assert lastfm._load_queue() == []
|
|
assert not (tmp_path / "scrobble_queue.json").exists()
|
|
|
|
def test_queue_capped(self, qapp, tmp_path):
|
|
prefs = Preferences(tmp_path)
|
|
lastfm = LastFm(prefs, tmp_path)
|
|
for i in range(600):
|
|
lastfm._enqueue({"artist": "A", "track": str(i), "timestamp": i})
|
|
queue = lastfm._load_queue()
|
|
assert len(queue) == 500
|
|
assert queue[-1]["track"] == "599"
|
|
|
|
|
|
class TestShuffleOrder:
|
|
def test_permutation_starts_at_current(self):
|
|
order = make_shuffle_order(10, 4)
|
|
assert order[0] == 4
|
|
assert sorted(order) == list(range(10))
|
|
|
|
def test_single_track(self):
|
|
assert make_shuffle_order(1, 0) == [0]
|
|
|
|
def test_empty(self):
|
|
assert make_shuffle_order(0, 0) == []
|
|
|
|
def test_is_randomized(self):
|
|
# With 20 tracks, two consecutive shuffles colliding entirely is
|
|
# vanishingly unlikely (1/19!)
|
|
orders = {tuple(make_shuffle_order(20, 0)) for _ in range(5)}
|
|
assert len(orders) > 1
|
|
|
|
|
|
class TestPreferences:
|
|
def test_defaults(self, qapp, tmp_path):
|
|
prefs = Preferences(tmp_path)
|
|
assert prefs.get("ui_scale") == "medium"
|
|
assert prefs.get("highlight") == "blue"
|
|
assert prefs.lastfm["scrobble_enabled"] is False
|
|
|
|
def test_round_trip(self, qapp, tmp_path):
|
|
prefs = Preferences(tmp_path)
|
|
prefs.set("ui_scale", "large")
|
|
prefs.update_lastfm(username="trav", session_key="abc")
|
|
|
|
reloaded = Preferences(tmp_path)
|
|
assert reloaded.get("ui_scale") == "large"
|
|
assert reloaded.lastfm["username"] == "trav"
|
|
assert reloaded.lastfm["session_key"] == "abc"
|
|
# Unknown/missing keys fall back to defaults
|
|
assert reloaded.get("highlight") == "blue"
|
|
|
|
def test_changed_signal(self, qapp, tmp_path):
|
|
prefs = Preferences(tmp_path)
|
|
fired = []
|
|
prefs.changed.connect(lambda: fired.append(1))
|
|
prefs.set("highlight", "seafoam")
|
|
prefs.set("highlight", "seafoam") # no-op, shouldn't fire again
|
|
assert len(fired) == 1
|
|
|
|
def test_corrupt_file_falls_back(self, qapp, tmp_path):
|
|
(tmp_path / "preferences.json").write_text("{not json")
|
|
prefs = Preferences(tmp_path)
|
|
assert prefs.get("ui_scale") == "medium"
|