324 lines
7.6 KiB
Go
324 lines
7.6 KiB
Go
package radio
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func newTestProvider(t *testing.T) *Provider {
|
|
t.Helper()
|
|
// Point HOME at a temp dir so New() doesn't touch real state.
|
|
t.Setenv("HOME", t.TempDir())
|
|
return New()
|
|
}
|
|
|
|
func TestProviderNewHasBuiltinStation(t *testing.T) {
|
|
p := newTestProvider(t)
|
|
if p.Name() != "Radio" {
|
|
t.Errorf("Name() = %q, want Radio", p.Name())
|
|
}
|
|
infos, err := p.Playlists()
|
|
if err != nil {
|
|
t.Fatalf("Playlists: %v", err)
|
|
}
|
|
if len(infos) == 0 {
|
|
t.Fatal("Playlists() returned none — expected built-in cliamp radio")
|
|
}
|
|
if infos[0].Name != builtinName {
|
|
t.Errorf("first playlist = %q, want %q", infos[0].Name, builtinName)
|
|
}
|
|
if !strings.HasPrefix(infos[0].ID, "l:") {
|
|
t.Errorf("first playlist ID = %q, want to start with 'l:'", infos[0].ID)
|
|
}
|
|
}
|
|
|
|
func TestProviderLoadsStationsFromTOML(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
|
|
// Create radios.toml with one extra station.
|
|
cfgDir := filepath.Join(home, ".config", "cliamp")
|
|
writeFile(t, filepath.Join(cfgDir, "radios.toml"), `[[station]]
|
|
name = "Extra"
|
|
url = "https://extra.example/stream"
|
|
`)
|
|
|
|
p := New()
|
|
infos, _ := p.Playlists()
|
|
if len(infos) < 2 {
|
|
t.Fatalf("expected builtin + extra, got %d", len(infos))
|
|
}
|
|
if infos[1].Name != "Extra" {
|
|
t.Errorf("second playlist = %q, want Extra", infos[1].Name)
|
|
}
|
|
}
|
|
|
|
func TestProviderTracksLocalStation(t *testing.T) {
|
|
p := newTestProvider(t)
|
|
infos, _ := p.Playlists()
|
|
tracks, err := p.Tracks(infos[0].ID)
|
|
if err != nil {
|
|
t.Fatalf("Tracks: %v", err)
|
|
}
|
|
if len(tracks) != 1 {
|
|
t.Fatalf("got %d tracks, want 1", len(tracks))
|
|
}
|
|
if tracks[0].Path != builtinURL {
|
|
t.Errorf("Path = %q, want %q", tracks[0].Path, builtinURL)
|
|
}
|
|
if !tracks[0].Stream || !tracks[0].Realtime {
|
|
t.Errorf("Stream/Realtime = %v/%v, want true/true", tracks[0].Stream, tracks[0].Realtime)
|
|
}
|
|
}
|
|
|
|
func TestProviderTracksInvalidID(t *testing.T) {
|
|
p := newTestProvider(t)
|
|
tests := []string{
|
|
"l:99",
|
|
"c:0", // catalog empty
|
|
"s:0", // search not active
|
|
"f:5",
|
|
"x:1",
|
|
"notanid",
|
|
"l:notanumber",
|
|
}
|
|
for _, id := range tests {
|
|
t.Run(id, func(t *testing.T) {
|
|
if _, err := p.Tracks(id); err == nil {
|
|
t.Errorf("Tracks(%q) = nil err, want error", id)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProviderCatalogLifecycle(t *testing.T) {
|
|
p := newTestProvider(t)
|
|
|
|
p.AppendCatalog([]CatalogStation{
|
|
{Name: "Radio A", URL: "http://a/", Bitrate: 192, Country: "NO"},
|
|
{Name: "Radio B", URL: "http://b/"},
|
|
})
|
|
|
|
infos, _ := p.Playlists()
|
|
// builtin (1) + catalog (2) = 3
|
|
if len(infos) != 3 {
|
|
t.Fatalf("len(infos) = %d, want 3 (builtin + 2 catalog)", len(infos))
|
|
}
|
|
if !strings.Contains(infos[1].ID, "c:0") {
|
|
t.Errorf("catalog id[0] = %q, want c:0", infos[1].ID)
|
|
}
|
|
|
|
// Tracks for catalog entry.
|
|
tracks, err := p.Tracks("c:0")
|
|
if err != nil {
|
|
t.Fatalf("Tracks(c:0): %v", err)
|
|
}
|
|
if tracks[0].Path != "http://a/" {
|
|
t.Errorf("Path = %q, want http://a/", tracks[0].Path)
|
|
}
|
|
|
|
// ToggleFavorite on a catalog entry should add it.
|
|
added, name, err := p.ToggleFavorite("c:0")
|
|
if err != nil {
|
|
t.Fatalf("ToggleFavorite: %v", err)
|
|
}
|
|
if !added || name != "Radio A" {
|
|
t.Errorf("ToggleFavorite = (%v, %q), want (true, Radio A)", added, name)
|
|
}
|
|
// Toggle again removes.
|
|
added, _, err = p.ToggleFavorite("c:0")
|
|
if err != nil {
|
|
t.Fatalf("ToggleFavorite 2: %v", err)
|
|
}
|
|
if added {
|
|
t.Error("second ToggleFavorite should remove")
|
|
}
|
|
}
|
|
|
|
func TestProviderToggleFavoriteLocalRejected(t *testing.T) {
|
|
p := newTestProvider(t)
|
|
_, _, err := p.ToggleFavorite("l:0")
|
|
if err == nil {
|
|
t.Error("ToggleFavorite on local station should error")
|
|
}
|
|
}
|
|
|
|
func TestProviderToggleFavoriteInvalidIdx(t *testing.T) {
|
|
p := newTestProvider(t)
|
|
_, _, err := p.ToggleFavorite("c:99")
|
|
if err == nil {
|
|
t.Error("ToggleFavorite on out-of-range catalog idx should error")
|
|
}
|
|
}
|
|
|
|
func TestProviderSearchLifecycle(t *testing.T) {
|
|
p := newTestProvider(t)
|
|
|
|
if p.IsSearching() {
|
|
t.Error("fresh provider should not be searching")
|
|
}
|
|
|
|
p.SetSearchResults([]CatalogStation{
|
|
{Name: "Hit", URL: "http://hit/"},
|
|
})
|
|
if !p.IsSearching() {
|
|
t.Error("SetSearchResults should put provider into searching mode")
|
|
}
|
|
|
|
infos, _ := p.Playlists()
|
|
if len(infos) != 1 || !strings.HasPrefix(infos[0].ID, "s:") {
|
|
t.Fatalf("Playlists during search = %+v, want single s:0 entry", infos)
|
|
}
|
|
|
|
// Tracks for a search result.
|
|
tracks, err := p.Tracks("s:0")
|
|
if err != nil {
|
|
t.Fatalf("Tracks(s:0): %v", err)
|
|
}
|
|
if tracks[0].Path != "http://hit/" {
|
|
t.Errorf("Path = %q, want http://hit/", tracks[0].Path)
|
|
}
|
|
|
|
p.ClearSearch()
|
|
if p.IsSearching() {
|
|
t.Error("ClearSearch should leave searching = false")
|
|
}
|
|
}
|
|
|
|
func TestIsCatalogOrFavID(t *testing.T) {
|
|
tests := []struct {
|
|
id string
|
|
want bool
|
|
}{
|
|
{"c:0", true},
|
|
{"f:3", true},
|
|
{"s:1", true},
|
|
{"l:0", false},
|
|
{"123", false},
|
|
{"", false},
|
|
}
|
|
for _, tt := range tests {
|
|
if got := IsCatalogOrFavID(tt.id); got != tt.want {
|
|
t.Errorf("IsCatalogOrFavID(%q) = %v, want %v", tt.id, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestProviderIDPrefix(t *testing.T) {
|
|
p := newTestProvider(t)
|
|
tests := []struct {
|
|
id string
|
|
want string
|
|
}{
|
|
{"c:0", "c"},
|
|
{"l:5", "l"},
|
|
{"f:3", "f"},
|
|
{"s:0", "s"},
|
|
{"noprefix", ""},
|
|
}
|
|
for _, tt := range tests {
|
|
if got := p.IDPrefix(tt.id); got != tt.want {
|
|
t.Errorf("IDPrefix(%q) = %q, want %q", tt.id, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestProviderIsFavoritableID(t *testing.T) {
|
|
p := newTestProvider(t)
|
|
if !p.IsFavoritableID("c:0") {
|
|
t.Error("c:0 should be favoritable")
|
|
}
|
|
if p.IsFavoritableID("l:0") {
|
|
t.Error("l:0 should not be favoritable")
|
|
}
|
|
}
|
|
|
|
func TestParseStationIDLegacyNumeric(t *testing.T) {
|
|
prefix, idx, err := parseStationID("5")
|
|
if err != nil {
|
|
t.Fatalf("parseStationID(5): %v", err)
|
|
}
|
|
if prefix != "l" || idx != 5 {
|
|
t.Errorf("parseStationID(5) = (%q, %d), want (l, 5)", prefix, idx)
|
|
}
|
|
}
|
|
|
|
func TestParseStationIDError(t *testing.T) {
|
|
_, _, err := parseStationID("c:notnumber")
|
|
if err == nil {
|
|
t.Error("parseStationID('c:notnumber') should error")
|
|
}
|
|
}
|
|
|
|
func TestFormatCatalogName(t *testing.T) {
|
|
tests := []struct {
|
|
in CatalogStation
|
|
want string
|
|
}{
|
|
{CatalogStation{Name: "Jazz"}, "Jazz"},
|
|
{CatalogStation{Name: "Jazz", Bitrate: 128}, "Jazz [128k]"},
|
|
{CatalogStation{Name: "Jazz", Country: "UK"}, "Jazz · UK"},
|
|
{CatalogStation{Name: "Jazz", Bitrate: 320, Country: "US"}, "Jazz [320k] · US"},
|
|
}
|
|
for _, tt := range tests {
|
|
if got := formatCatalogName(tt.in); got != tt.want {
|
|
t.Errorf("formatCatalogName(%+v) = %q, want %q", tt.in, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLoadStationsIgnoresMissingFile(t *testing.T) {
|
|
stations, err := loadStations(filepath.Join(t.TempDir(), "nope.toml"))
|
|
if err != nil {
|
|
t.Errorf("loadStations on missing file should not error, got %v", err)
|
|
}
|
|
if stations != nil {
|
|
t.Errorf("missing file should return nil stations, got %+v", stations)
|
|
}
|
|
}
|
|
|
|
func TestLoadStationsParsesMultiple(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "radios.toml")
|
|
writeFile(t, path, `# leading comment
|
|
[[station]]
|
|
name = "A"
|
|
url = "http://a/"
|
|
|
|
[[station]]
|
|
name = "B"
|
|
url = "http://b/"
|
|
# trailing comment
|
|
`)
|
|
stations, err := loadStations(path)
|
|
if err != nil {
|
|
t.Fatalf("loadStations: %v", err)
|
|
}
|
|
if len(stations) != 2 {
|
|
t.Fatalf("len = %d, want 2", len(stations))
|
|
}
|
|
if stations[0].name != "A" || stations[1].name != "B" {
|
|
t.Errorf("names = %+v", stations)
|
|
}
|
|
}
|
|
|
|
func TestLoadStationsSkipsIncompleteEntries(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "radios.toml")
|
|
writeFile(t, path, `[[station]]
|
|
name = "no-url"
|
|
|
|
[[station]]
|
|
name = "complete"
|
|
url = "http://c/"
|
|
`)
|
|
stations, err := loadStations(path)
|
|
if err != nil {
|
|
t.Fatalf("loadStations: %v", err)
|
|
}
|
|
if len(stations) != 1 || stations[0].name != "complete" {
|
|
t.Errorf("expected only 'complete', got %+v", stations)
|
|
}
|
|
}
|