From 58e9202e2557dadfca630a43da415241304c059a Mon Sep 17 00:00:00 2001 From: beefcarver Date: Tue, 3 Nov 2026 16:10:39 -0500 Subject: [PATCH] test(providers): cover the registry and the app's catalogue independence Registry lookup and its fallback for a stale slug, SomaFM branding and guessed mirrors, provider round-trip in the config, legacy-config migration and BEEFY_*/SOMAFM_* precedence - plus an end-to-end boot against a fake second provider, which is what actually proves the UI no longer knows about SomaFM. --- tests/test_providers.py | 130 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 tests/test_providers.py diff --git a/tests/test_providers.py b/tests/test_providers.py new file mode 100644 index 0000000..7ad5799 --- /dev/null +++ b/tests/test_providers.py @@ -0,0 +1,130 @@ +from __future__ import annotations + +import html +import json +import re + +import pytest + +from beefy_player import config +from beefy_player.models import Song, Station +from beefy_player.providers import PROVIDERS, available_providers, get_provider +from beefy_player.providers.base import RadioProvider +from beefy_player.providers.somafm import SomaFMProvider + + +def rendered_text(app) -> str: + svg = html.unescape(app.export_screenshot()).replace("\xa0", " ") + return re.sub(r"<[^>]+>", " ", svg) + + +def test_registry_lists_somafm_as_the_default() -> None: + assert available_providers() == ("somafm",) + assert isinstance(get_provider(), SomaFMProvider) + assert get_provider("somafm") is get_provider() + # a stale slug in the config file must not stop the app from starting + assert get_provider("no-such-catalogue") is get_provider() + + +def test_somafm_provider_describes_its_catalogue() -> None: + provider = get_provider() + assert provider.name == "SomaFM" + assert provider.brand == "SOMA FM" + assert provider.station_source == "somafm.com/channels.json" + assert "somafm.com" in provider.user_agent + + stations = provider.fallback_stations() + assert len(stations) > 40 + station = stations[0] + assert station.provider == "somafm" + assert station.uid == f"somafm:{station.id}" + assert all("somafm.com" in url for url in provider.fallback_urls(station)) + + +class FakeProvider(RadioProvider): + """A second catalogue, as small as the interface allows.""" + + id = "fake" + name = "Fake FM" + homepage = "fake.test" + brand = "FAKE FM" + tagline = "not real radio" + station_source = "fake.test/stations.json" + history_source = "recent.json" + + def __init__(self) -> None: + self.station = Station( + id="one", title="Fake One", genre="test", provider=self.id + ) + + async def fetch_stations(self, client): + return [self.station] + + def load_cached_stations(self): + return [self.station] + + def fallback_stations(self): + return [self.station] + + async def fetch_history(self, client, station_id): + return [Song(title="Tune", artist="Someone")] + + def load_cached_history(self, station_id): + return [] + + +@pytest.mark.asyncio +async def test_app_runs_against_a_different_catalogue(monkeypatch) -> None: + """The UI must not know which catalogue it is showing.""" + from beefy_player.app import BeefyPlayerApp + + fake = FakeProvider() + monkeypatch.setitem(PROVIDERS, "fake", fake) + app = BeefyPlayerApp(provider="fake", autoplay=False, offline=True, audio=False) + assert app.provider is fake + async with app.run_test(size=(100, 30)) as pilot: + await pilot.pause() + assert app.current is not None + assert app.current.uid == "fake:one" + app.query_one("StagePanel").refresh_stage() + await pilot.pause() + text = rendered_text(app) + assert "FAKE ONE" in text + assert "Fake FM" in app.title + + +def test_config_remembers_the_provider() -> None: + assert config.Config.load().provider == "somafm" + config.Config(provider="fake").save() + assert config.Config.load().provider == "fake" + + +def test_legacy_config_is_migrated_once(monkeypatch) -> None: + legacy = config.LEGACY_CONFIG_PATH + legacy.parent.mkdir(parents=True, exist_ok=True) + legacy.write_text( + json.dumps({"favorites": ["defcon"], "volume": 33, "last_station": "dronezone"}) + ) + + loaded = config.Config.load() + assert loaded.favorites == ["defcon"] + assert loaded.volume == 33 + assert loaded.last_station == "dronezone" + assert config.CONFIG_PATH.exists() + + # the new file wins from then on + config.Config(volume=11).save() + legacy.write_text(json.dumps({"volume": 99})) + assert config.Config.load().volume == 11 + + +def test_environment_variables_prefer_the_beefy_prefix(monkeypatch) -> None: + monkeypatch.setenv("BEEFY_IMAGES", "tgp") + monkeypatch.setenv("SOMAFM_IMAGES", "0") + assert config.env_var("IMAGES") == "tgp" + + monkeypatch.delenv("BEEFY_IMAGES") + assert config.env_var("IMAGES") == "0" # the old spelling still works + + monkeypatch.delenv("SOMAFM_IMAGES") + assert config.env_var("IMAGES") == ""