| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741 |
- # -*- coding: utf-8 -*-
- """
- Tests fuer die Radio-Quelle (RadioSource) und RadioStation.
- Testet HTTP-Radio-Streaming mit ICY-Metadaten-Unterstuetzung
- sowie die Fuzzy-Suche ueber Radio-Sender.
- """
- from __future__ import annotations
- import asyncio
- from pathlib import Path
- from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock
- import pytest
- from trixy_core.music.sources.base import SourceState, SourceType, SourceInfo
- from trixy_core.music.track import Track, TrackMetadata, TrackState
- # =============================================================================
- # Hilfsfunktionen und Fixtures
- # =============================================================================
- def _make_station_dict(
- name: str = "TestRadio",
- url: str = "https://stream.testradio.de/live.mp3",
- genre: str = "Pop",
- country: str = "DE",
- description: str = "",
- ) -> dict[str, str]:
- """Erzeugt ein Sender-Dict fuer RadioSource-Konfiguration."""
- return {
- "name": name,
- "url": url,
- "genre": genre,
- "country": country,
- "description": description,
- }
- @pytest.fixture
- def radio_source():
- """Erstellt eine RadioSource-Instanz mit Standardwerten."""
- with patch("plugins.music.sources.radio._AIOHTTP_AVAILABLE", True):
- from plugins.music.sources.radio import RadioSource
- return RadioSource(
- favorites=[
- _make_station_dict("SWR3", "https://swr3.de/live.mp3", "Pop"),
- _make_station_dict("1LIVE", "https://1live.de/live.mp3", "Pop/Rock"),
- _make_station_dict("Fritz", "https://fritz.de/live.mp3", "Alternative", description="Berliner Jugendradio"),
- ],
- include_defaults=False,
- )
- @pytest.fixture
- async def connected_radio(radio_source):
- """Erstellt eine verbundene RadioSource."""
- await radio_source.connect()
- return radio_source
- # =============================================================================
- # RadioStation-Tests
- # =============================================================================
- class TestRadioStation:
- """Tests fuer die RadioStation-Datenklasse."""
- def test_erstellung(self):
- """RadioStation kann mit Name und URL erstellt werden."""
- from plugins.music.sources.radio import RadioStation
- station = RadioStation(name="TestFM", url="https://test.fm/stream.mp3")
- assert station.name == "TestFM"
- assert station.url == "https://test.fm/stream.mp3"
- assert station.genre == ""
- assert station.country == "DE"
- assert station.description == ""
- def test_erstellung_mit_allen_feldern(self):
- """RadioStation akzeptiert alle optionalen Felder."""
- from plugins.music.sources.radio import RadioStation
- station = RadioStation(
- name="RockFM",
- url="https://rock.fm/live.mp3",
- genre="Rock",
- country="AT",
- description="Oesterreichisches Rockradio",
- )
- assert station.genre == "Rock"
- assert station.country == "AT"
- assert station.description == "Oesterreichisches Rockradio"
- def test_to_track_konvertierung(self):
- """to_track() erzeugt korrektes Track-Objekt."""
- from plugins.music.sources.radio import RadioStation
- station = RadioStation(
- name="JazzFM",
- url="https://jazz.fm/stream.mp3",
- genre="Jazz",
- country="DE",
- description="Jazz und Swing",
- )
- track = station.to_track()
- assert isinstance(track, Track)
- assert track.uri == "radio:JazzFM"
- assert track.source_type == "radio"
- assert track.source_id == "JazzFM"
- assert track.state == TrackState.AVAILABLE
- def test_to_track_metadaten(self):
- """to_track() setzt korrekte Metadaten-Felder."""
- from plugins.music.sources.radio import RadioStation
- station = RadioStation(
- name="KlassikFM",
- url="https://klassik.fm/stream.mp3",
- genre="Klassik",
- )
- track = station.to_track()
- assert track.metadata.title == "KlassikFM"
- assert track.metadata.artist == "Live Radio"
- assert track.metadata.genre == "Klassik"
- assert track.metadata.duration_ms == 0 # Live = keine Dauer
- def test_to_track_custom_felder(self):
- """to_track() setzt stream_url und country in custom."""
- from plugins.music.sources.radio import RadioStation
- station = RadioStation(name="Test", url="https://test.de/s.mp3", country="CH")
- track = station.to_track()
- assert track.metadata.custom["country"] == "CH"
- assert track.metadata.custom["stream_url"] == "https://test.de/s.mp3"
- def test_to_track_extra_felder(self):
- """to_track() setzt is_live, genre, country und description in extra."""
- from plugins.music.sources.radio import RadioStation
- station = RadioStation(
- name="TestFM", url="https://test.fm/s.mp3",
- genre="Pop", country="DE", description="Testbeschreibung",
- )
- track = station.to_track()
- assert track.extra["stream_url"] == "https://test.fm/s.mp3"
- assert track.extra["is_live"] is True
- assert track.extra["genre"] == "Pop"
- assert track.extra["country"] == "DE"
- assert track.extra["description"] == "Testbeschreibung"
- # =============================================================================
- # connect()-Tests
- # =============================================================================
- class TestRadioSourceConnect:
- """Tests fuer die connect()-Methode."""
- @pytest.mark.asyncio
- async def test_aiohttp_nicht_verfuegbar(self):
- """connect() gibt False zurueck wenn aiohttp nicht installiert."""
- with patch("plugins.music.sources.radio._AIOHTTP_AVAILABLE", False):
- from plugins.music.sources.radio import RadioSource
- source = RadioSource()
- result = await source.connect()
- assert result is False
- assert source.state == SourceState.UNAVAILABLE
- @pytest.mark.asyncio
- async def test_mit_favoriten(self):
- """connect() laedt Favoriten-Sender korrekt."""
- with patch("plugins.music.sources.radio._AIOHTTP_AVAILABLE", True):
- from plugins.music.sources.radio import RadioSource
- source = RadioSource(
- favorites=[
- _make_station_dict("FM1", "https://fm1.de/s.mp3", "Pop"),
- _make_station_dict("FM2", "https://fm2.de/s.mp3", "Rock"),
- ],
- include_defaults=False,
- )
- result = await source.connect()
- assert result is True
- assert source.state == SourceState.AVAILABLE
- assert source.station_count == 2
- @pytest.mark.asyncio
- async def test_keine_favoriten_mit_defaults(self):
- """connect() laedt Standard-Sender wenn keine Favoriten."""
- with patch("plugins.music.sources.radio._AIOHTTP_AVAILABLE", True):
- from plugins.music.sources.radio import RadioSource
- source = RadioSource(favorites=[], include_defaults=True)
- result = await source.connect()
- assert result is True
- assert source.station_count == 13 # 13 Standard-Sender
- @pytest.mark.asyncio
- async def test_keine_favoriten_ohne_defaults(self):
- """connect() gibt False zurueck ohne Sender."""
- with patch("plugins.music.sources.radio._AIOHTTP_AVAILABLE", True):
- from plugins.music.sources.radio import RadioSource
- source = RadioSource(favorites=[], include_defaults=False)
- result = await source.connect()
- assert result is False
- assert source.state == SourceState.UNAVAILABLE
- @pytest.mark.asyncio
- async def test_ungueltige_favoriten_werden_uebersprungen(self):
- """connect() ueberspringt Favoriten ohne Name oder URL."""
- with patch("plugins.music.sources.radio._AIOHTTP_AVAILABLE", True):
- from plugins.music.sources.radio import RadioSource
- source = RadioSource(
- favorites=[
- {"name": "", "url": "https://no-name.de/s.mp3"},
- {"name": "NoURL", "url": ""},
- _make_station_dict("Gueltig", "https://ok.de/s.mp3"),
- ],
- include_defaults=False,
- )
- result = await source.connect()
- assert result is True
- assert source.station_count == 1
- @pytest.mark.asyncio
- async def test_connect_registriert_tracks(self):
- """connect() fuegt Tracks zum internen _tracks-Dict hinzu."""
- with patch("plugins.music.sources.radio._AIOHTTP_AVAILABLE", True):
- from plugins.music.sources.radio import RadioSource
- source = RadioSource(
- favorites=[_make_station_dict("TestFM", "https://test.fm/s.mp3")],
- include_defaults=False,
- )
- await source.connect()
- assert source.track_count == 1
- # =============================================================================
- # disconnect()-Tests
- # =============================================================================
- class TestRadioSourceDisconnect:
- """Tests fuer die disconnect()-Methode."""
- @pytest.mark.asyncio
- async def test_disconnect_loescht_zustand(self, connected_radio):
- """disconnect() loescht Sender, Tracks und Metadaten."""
- source = connected_radio
- assert source.station_count > 0
- await source.disconnect()
- assert source.station_count == 0
- assert source.track_count == 0
- assert source.current_metadata == {}
- assert source.state == SourceState.UNAVAILABLE
- # =============================================================================
- # Properties-Tests
- # =============================================================================
- class TestRadioSourceProperties:
- """Tests fuer station_count, stations und current_metadata."""
- @pytest.mark.asyncio
- async def test_station_count(self, connected_radio):
- """station_count gibt korrekte Anzahl zurueck."""
- assert connected_radio.station_count == 3
- @pytest.mark.asyncio
- async def test_stations_defensive_kopie(self, connected_radio):
- """stations gibt eine Kopie zurueck (keine Referenz)."""
- source = connected_radio
- stations = source.stations
- original_count = len(stations)
- stations.append(MagicMock()) # Aendern der Kopie
- assert len(source.stations) == original_count
- @pytest.mark.asyncio
- async def test_current_metadata_initial_leer(self, connected_radio):
- """current_metadata ist initial leer."""
- assert connected_radio.current_metadata == {}
- @pytest.mark.asyncio
- async def test_current_metadata_defensive_kopie(self, connected_radio):
- """current_metadata gibt eine Kopie zurueck."""
- source = connected_radio
- source._current_metadata = {"artist": "Test", "title": "Song"}
- meta = source.current_metadata
- meta["extra"] = "Hack"
- assert "extra" not in source.current_metadata
- # =============================================================================
- # search()-Tests
- # =============================================================================
- class TestRadioSourceSearch:
- """Tests fuer die search()-Methode."""
- @pytest.mark.asyncio
- async def test_exakter_name_match(self, connected_radio):
- """search() gibt Score 100 bei exaktem Namens-Match."""
- results = await connected_radio.search("SWR3")
- assert len(results) >= 1
- assert results[0].metadata.title == "SWR3"
- @pytest.mark.asyncio
- async def test_name_enthaelt_query(self, connected_radio):
- """search() findet Sender wenn Name den Query enthaelt."""
- results = await connected_radio.search("LIVE")
- assert len(results) >= 1
- found_names = [r.metadata.title for r in results]
- assert "1LIVE" in found_names
- @pytest.mark.asyncio
- async def test_genre_match(self, connected_radio):
- """search() findet Sender ueber Genre-Match."""
- results = await connected_radio.search("Alternative")
- assert len(results) >= 1
- found_names = [r.metadata.title for r in results]
- assert "Fritz" in found_names
- @pytest.mark.asyncio
- async def test_beschreibung_match(self, connected_radio):
- """search() findet Sender ueber Beschreibung."""
- results = await connected_radio.search("Berliner")
- assert len(results) >= 1
- found_names = [r.metadata.title for r in results]
- assert "Fritz" in found_names
- @pytest.mark.asyncio
- async def test_wort_teilmatch(self, connected_radio):
- """search() findet Sender bei Wort-Teilmatch im Namen."""
- results = await connected_radio.search("SWR Live")
- # "SWR" passt auf SWR3 (Wort-Match)
- found_names = [r.metadata.title for r in results]
- assert "SWR3" in found_names
- @pytest.mark.asyncio
- async def test_kein_match(self, connected_radio):
- """search() gibt leere Liste bei keinem Treffer zurueck."""
- results = await connected_radio.search("XYZGibtEsNicht")
- assert results == []
- @pytest.mark.asyncio
- async def test_respektiert_limit(self, connected_radio):
- """search() begrenzt Ergebnisse auf limit."""
- # Alle drei Sender haben "Pop" im Genre oder enthalten Teilmatch
- results = await connected_radio.search("Pop", limit=1)
- assert len(results) <= 1
- @pytest.mark.asyncio
- async def test_case_insensitive(self, connected_radio):
- """search() ist gross-/kleinschreibungsunabhaengig."""
- results_upper = await connected_radio.search("SWR3")
- results_lower = await connected_radio.search("swr3")
- assert len(results_upper) == len(results_lower)
- assert results_upper[0].metadata.title == results_lower[0].metadata.title
- @pytest.mark.asyncio
- async def test_sortierung_nach_score(self, connected_radio):
- """search() sortiert Ergebnisse nach Score (hoechster zuerst)."""
- # "Fritz" — exakter Name-Match = 100
- # Wenn wir nach "Fritz" suchen, sollte Fritz zuerst kommen
- results = await connected_radio.search("Fritz")
- assert len(results) >= 1
- assert results[0].metadata.title == "Fritz"
- @pytest.mark.asyncio
- async def test_suche_nicht_verbunden(self):
- """search() gibt leere Liste zurueck wenn nicht verbunden."""
- with patch("plugins.music.sources.radio._AIOHTTP_AVAILABLE", True):
- from plugins.music.sources.radio import RadioSource
- source = RadioSource(include_defaults=False)
- # Nicht verbunden (state = UNKNOWN)
- results = await source.search("SWR3")
- assert results == []
- @pytest.mark.asyncio
- async def test_score_prioritaet(self):
- """search() priorisiert exakten Name > Contains > Genre > Beschreibung."""
- with patch("plugins.music.sources.radio._AIOHTTP_AVAILABLE", True):
- from plugins.music.sources.radio import RadioSource
- source = RadioSource(
- favorites=[
- _make_station_dict("Jazz", "https://a.de/s.mp3", "Rock", description="Kein Jazz hier"),
- _make_station_dict("RockJazz", "https://b.de/s.mp3", "Jazz"),
- _make_station_dict("Pop", "https://c.de/s.mp3", "Pop", description="Jazz Abende"),
- ],
- include_defaults=False,
- )
- await source.connect()
- results = await source.search("Jazz")
- assert len(results) == 3
- # Exakt (Jazz) → Contains (RockJazz) → Genre (RockJazz hat Jazz-Genre) oder Beschreibung
- assert results[0].metadata.title == "Jazz" # Score 100
- # =============================================================================
- # prepare_track()-Tests
- # =============================================================================
- class TestRadioSourcePrepareTrack:
- """Tests fuer die prepare_track()-Methode."""
- @pytest.mark.asyncio
- async def test_mit_stream_url(self, connected_radio):
- """prepare_track() gibt True zurueck wenn stream_url vorhanden."""
- track = Track(
- source_type="radio",
- source_id="TestFM",
- extra={"stream_url": "https://stream.testfm.de/live.mp3"},
- )
- result = await connected_radio.prepare_track(track)
- assert result is True
- assert track.state == TrackState.AVAILABLE
- @pytest.mark.asyncio
- async def test_ohne_stream_url(self, connected_radio):
- """prepare_track() gibt False zurueck ohne stream_url."""
- track = Track(
- source_type="radio",
- source_id="NoStream",
- extra={},
- )
- result = await connected_radio.prepare_track(track)
- assert result is False
- assert track.state == TrackState.ERROR
- assert "Keine Stream-URL" in track.error_message
- @pytest.mark.asyncio
- async def test_leere_stream_url(self, connected_radio):
- """prepare_track() gibt False zurueck bei leerer stream_url."""
- track = Track(
- source_type="radio",
- source_id="EmptyURL",
- extra={"stream_url": ""},
- )
- result = await connected_radio.prepare_track(track)
- assert result is False
- assert track.state == TrackState.ERROR
- # =============================================================================
- # _parse_icy_metadata()-Tests
- # =============================================================================
- class TestRadioSourceIcyMetadata:
- """Tests fuer die ICY-Metadaten-Verarbeitung."""
- @pytest.mark.asyncio
- async def test_artist_title_format(self, connected_radio):
- """_parse_icy_metadata() parst 'Artist - Title' korrekt."""
- track = Track(source_type="radio", extra={"station_name": "TestFM"})
- connected_radio._parse_icy_metadata(
- "StreamTitle='Die Toten Hosen - Tage wie diese';", track
- )
- meta = connected_radio.current_metadata
- assert meta["artist"] == "Die Toten Hosen"
- assert meta["title"] == "Tage wie diese"
- @pytest.mark.asyncio
- async def test_nur_titel(self, connected_radio):
- """_parse_icy_metadata() setzt nur title wenn kein Separator."""
- track = Track(source_type="radio", extra={})
- connected_radio._parse_icy_metadata(
- "StreamTitle='Nachrichten um 12';", track
- )
- meta = connected_radio.current_metadata
- assert meta["artist"] == ""
- assert meta["title"] == "Nachrichten um 12"
- @pytest.mark.asyncio
- async def test_leerer_streamtitle(self, connected_radio):
- """_parse_icy_metadata() ignoriert leeren StreamTitle."""
- connected_radio._current_metadata = {"artist": "Alt", "title": "Vorher"}
- track = Track(source_type="radio", extra={})
- connected_radio._parse_icy_metadata("StreamTitle='';", track)
- # Metadaten sollten nicht aktualisiert werden
- assert connected_radio.current_metadata["artist"] == "Alt"
- @pytest.mark.asyncio
- async def test_kein_streamtitle_match(self, connected_radio):
- """_parse_icy_metadata() macht nichts ohne StreamTitle-Pattern."""
- connected_radio._current_metadata = {"artist": "Orig", "title": "Song"}
- track = Track(source_type="radio", extra={})
- connected_radio._parse_icy_metadata("SomeOtherData=value;", track)
- assert connected_radio.current_metadata["artist"] == "Orig"
- @pytest.mark.asyncio
- async def test_metadaten_werden_aktualisiert(self, connected_radio):
- """current_metadata wird nach parse aktualisiert."""
- assert connected_radio.current_metadata == {}
- track = Track(source_type="radio", extra={})
- connected_radio._parse_icy_metadata(
- "StreamTitle='Rammstein - Du Hast';", track
- )
- meta = connected_radio.current_metadata
- assert meta["artist"] == "Rammstein"
- assert meta["title"] == "Du Hast"
- @pytest.mark.asyncio
- async def test_track_custom_felder_aktualisiert(self, connected_radio):
- """_parse_icy_metadata() aktualisiert Track-Metadaten custom-Felder."""
- track = Track(
- source_type="radio",
- metadata=TrackMetadata(title="TestFM", custom={}),
- extra={"station_name": "TestFM"},
- )
- connected_radio._parse_icy_metadata(
- "StreamTitle='Seeed - Dickes B';", track
- )
- assert track.metadata.custom["now_playing_artist"] == "Seeed"
- assert track.metadata.custom["now_playing_title"] == "Dickes B"
- # =============================================================================
- # check_stream()-Tests
- # =============================================================================
- class TestRadioSourceCheckStream:
- """Tests fuer die check_stream()-Methode."""
- @pytest.mark.asyncio
- async def test_stream_erreichbar(self, connected_radio):
- """check_stream() gibt True zurueck bei HTTP 200."""
- import plugins.music.sources.radio as radio_mod
- mock_resp = MagicMock()
- mock_resp.status = 200
- # session.head(url) muss ein async context manager sein
- mock_head_ctx = AsyncMock()
- mock_head_ctx.__aenter__.return_value = mock_resp
- mock_head_ctx.__aexit__.return_value = False
- mock_session = MagicMock()
- mock_session.head.return_value = mock_head_ctx
- # ClientSession() muss ein async context manager sein
- mock_session_ctx = AsyncMock()
- mock_session_ctx.__aenter__.return_value = mock_session
- mock_session_ctx.__aexit__.return_value = False
- mock_aiohttp = MagicMock()
- mock_aiohttp.ClientTimeout = MagicMock()
- mock_aiohttp.ClientSession.return_value = mock_session_ctx
- with patch.object(radio_mod, "aiohttp", mock_aiohttp):
- result = await connected_radio.check_stream("https://stream.test.de/live.mp3")
- assert result is True
- @pytest.mark.asyncio
- async def test_stream_nicht_erreichbar(self, connected_radio):
- """check_stream() gibt False zurueck bei Verbindungsfehler."""
- import plugins.music.sources.radio as radio_mod
- # ClientSession als async context manager, head() wirft Exception
- mock_session = MagicMock()
- mock_session.head.side_effect = Exception("Connection refused")
- mock_session_ctx = AsyncMock()
- mock_session_ctx.__aenter__.return_value = mock_session
- mock_session_ctx.__aexit__.return_value = False
- mock_aiohttp = MagicMock()
- mock_aiohttp.ClientTimeout = MagicMock()
- mock_aiohttp.ClientSession.return_value = mock_session_ctx
- with patch.object(radio_mod, "aiohttp", mock_aiohttp):
- result = await connected_radio.check_stream("https://down.test.de/s.mp3")
- assert result is False
- @pytest.mark.asyncio
- async def test_check_stream_aiohttp_fehlt(self):
- """check_stream() gibt False zurueck ohne aiohttp."""
- with patch("plugins.music.sources.radio._AIOHTTP_AVAILABLE", False):
- from plugins.music.sources.radio import RadioSource
- source = RadioSource()
- result = await source.check_stream("https://test.de/s.mp3")
- assert result is False
- # =============================================================================
- # get_info()-Tests
- # =============================================================================
- class TestRadioSourceGetInfo:
- """Tests fuer die get_info()-Methode."""
- @pytest.mark.asyncio
- async def test_get_info_felder(self, connected_radio):
- """get_info() gibt korrekte SourceInfo zurueck.
- Hinweis: radio.py nutzt self._source_type / self._name statt
- self.source_type / self.name — bekannter Bug. Wir setzen die
- privaten Attribute damit get_info() nicht abstuerzt.
- """
- # Workaround fuer Bug: _source_type / _name statt source_type / name
- connected_radio._source_type = connected_radio.source_type
- connected_radio._name = connected_radio.name
- info = connected_radio.get_info()
- assert isinstance(info, SourceInfo)
- assert info.name == "Radio"
- assert info.state == SourceState.AVAILABLE
- assert info.track_count == 3
- assert info.supports_search is True
- assert info.supports_streaming is True
- assert info.supports_artwork is False
- assert "station_count" in info.metadata
- assert info.metadata["station_count"] == 3
- @pytest.mark.asyncio
- async def test_get_info_sender_liste(self, connected_radio):
- """get_info() enthaelt Sender-Namen in metadata."""
- # Workaround fuer Bug: _source_type / _name statt source_type / name
- connected_radio._source_type = connected_radio.source_type
- connected_radio._name = connected_radio.name
- info = connected_radio.get_info()
- station_names = info.metadata["stations"]
- assert "SWR3" in station_names
- assert "1LIVE" in station_names
- assert "Fritz" in station_names
- # =============================================================================
- # scan()-Tests
- # =============================================================================
- class TestRadioSourceScan:
- """Tests fuer die scan()-Methode."""
- @pytest.mark.asyncio
- async def test_scan_gibt_sender_anzahl(self, connected_radio):
- """scan() gibt Anzahl der Sender zurueck."""
- result = await connected_radio.scan()
- assert result == 3
- # =============================================================================
- # get_track()-Tests
- # =============================================================================
- class TestRadioSourceGetTrack:
- """Tests fuer die get_track()-Methode."""
- @pytest.mark.asyncio
- async def test_track_nach_id(self, connected_radio):
- """get_track() findet registrierten Track nach ID."""
- # Tracks werden beim connect() registriert
- tracks = connected_radio.tracks
- assert len(tracks) > 0
- first_track = tracks[0]
- result = await connected_radio.get_track(first_track.id)
- assert result is not None
- assert result.id == first_track.id
- @pytest.mark.asyncio
- async def test_track_nicht_gefunden(self, connected_radio):
- """get_track() gibt None zurueck bei unbekannter ID."""
- result = await connected_radio.get_track("unbekannte_id_xyz")
- assert result is None
- # =============================================================================
- # _DEFAULT_STATIONS-Tests
- # =============================================================================
- class TestDefaultStations:
- """Tests fuer die vordefinierten Standard-Sender."""
- def test_anzahl_default_sender(self):
- """_DEFAULT_STATIONS enthaelt 13 Sender."""
- from plugins.music.sources.radio import _DEFAULT_STATIONS
- assert len(_DEFAULT_STATIONS) == 13
- def test_default_sender_haben_name_und_url(self):
- """Alle Standard-Sender haben name und url."""
- from plugins.music.sources.radio import _DEFAULT_STATIONS
- for station in _DEFAULT_STATIONS:
- assert "name" in station and station["name"], f"Sender ohne Name: {station}"
- assert "url" in station and station["url"], f"Sender ohne URL: {station}"
- def test_bekannte_sender_vorhanden(self):
- """Wichtige deutsche Sender sind in den Defaults enthalten."""
- from plugins.music.sources.radio import _DEFAULT_STATIONS
- namen = [s["name"] for s in _DEFAULT_STATIONS]
- assert "SWR3" in namen
- assert "1LIVE" in namen
- assert "Deutschlandfunk" in namen
- assert "Klassik Radio" in namen
- assert "FluxFM" in namen
- def test_default_sender_haben_genre(self):
- """Alle Standard-Sender haben ein Genre."""
- from plugins.music.sources.radio import _DEFAULT_STATIONS
- for station in _DEFAULT_STATIONS:
- assert "genre" in station and station["genre"], f"Sender ohne Genre: {station['name']}"
- # =============================================================================
- # Konstruktor-Tests
- # =============================================================================
- class TestRadioSourceKonstruktor:
- """Tests fuer den RadioSource-Konstruktor."""
- def test_standardwerte(self):
- """Konstruktor setzt korrekte Standardwerte."""
- with patch("plugins.music.sources.radio._AIOHTTP_AVAILABLE", True):
- from plugins.music.sources.radio import RadioSource
- source = RadioSource()
- assert source.source_type == SourceType.URL
- assert source.name == "Radio"
- assert source._buffer_size == 16384
- assert source._icy_metadata is True
- assert source._include_defaults is True
- def test_benutzerdefinierte_werte(self):
- """Konstruktor akzeptiert benutzerdefinierte Parameter."""
- with patch("plugins.music.sources.radio._AIOHTTP_AVAILABLE", True):
- from plugins.music.sources.radio import RadioSource
- source = RadioSource(
- name="MeinRadio",
- buffer_size=32768,
- icy_metadata=False,
- include_defaults=False,
- )
- assert source.name == "MeinRadio"
- assert source._buffer_size == 32768
- assert source._icy_metadata is False
- assert source._include_defaults is False
- def test_anfangszustand(self):
- """Anfangszustand ist UNKNOWN."""
- with patch("plugins.music.sources.radio._AIOHTTP_AVAILABLE", True):
- from plugins.music.sources.radio import RadioSource
- source = RadioSource()
- assert source.state == SourceState.UNKNOWN
- assert source.station_count == 0
- assert source.current_metadata == {}
|