605 lines
16 KiB
Go
605 lines
16 KiB
Go
package navidrome
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/bjarneo/cliamp/config"
|
|
"github.com/bjarneo/cliamp/playlist"
|
|
"github.com/bjarneo/cliamp/provider"
|
|
)
|
|
|
|
// subsonicHandler serves fake Subsonic API responses for testing.
|
|
func subsonicHandler(t *testing.T) http.HandlerFunc {
|
|
t.Helper()
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
path := r.URL.Path
|
|
switch {
|
|
case strings.HasSuffix(path, "/rest/getPlaylists"):
|
|
w.Write([]byte(`{
|
|
"subsonic-response": {
|
|
"status": "ok",
|
|
"playlists": {
|
|
"playlist": [
|
|
{"id":"pl-1","name":"Jazz Classics","songCount":12},
|
|
{"id":"pl-2","name":"Late Night","songCount":8}
|
|
]
|
|
}
|
|
}
|
|
}`))
|
|
case strings.HasSuffix(path, "/rest/getPlaylist"):
|
|
w.Write([]byte(`{
|
|
"subsonic-response": {
|
|
"status": "ok",
|
|
"playlist": {
|
|
"entry": [
|
|
{
|
|
"id":"song-1","title":"So What","artist":"Miles Davis",
|
|
"album":"Kind of Blue","year":1959,"track":1,"genre":"Jazz","duration":565
|
|
},
|
|
{
|
|
"id":"song-2","title":"Blue in Green","artist":"Miles Davis",
|
|
"album":"Kind of Blue","year":1959,"track":3,"genre":"Jazz","duration":327
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}`))
|
|
case strings.HasSuffix(path, "/rest/getArtists"):
|
|
w.Write([]byte(`{
|
|
"subsonic-response": {
|
|
"status": "ok",
|
|
"artists": {
|
|
"index": [
|
|
{
|
|
"artist": [
|
|
{"id":"ar-1","name":"Miles Davis","albumCount":5},
|
|
{"id":"ar-2","name":"Mingus","albumCount":3}
|
|
]
|
|
},
|
|
{
|
|
"artist": [
|
|
{"id":"ar-3","name":"Thelonious Monk","albumCount":4}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}`))
|
|
case strings.HasSuffix(path, "/rest/getArtist"):
|
|
w.Write([]byte(`{
|
|
"subsonic-response": {
|
|
"status": "ok",
|
|
"artist": {
|
|
"album": [
|
|
{"id":"al-1","name":"Kind of Blue","artist":"Miles Davis","artistId":"ar-1","year":1959,"songCount":5,"genre":"Jazz"},
|
|
{"id":"al-2","name":"Bitches Brew","artist":"Miles Davis","artistId":"ar-1","year":1970,"songCount":4,"genre":"Jazz"}
|
|
]
|
|
}
|
|
}
|
|
}`))
|
|
case strings.HasSuffix(path, "/rest/getAlbumList2"):
|
|
w.Write([]byte(`{
|
|
"subsonic-response": {
|
|
"status": "ok",
|
|
"albumList2": {
|
|
"album": [
|
|
{"id":"al-1","name":"Kind of Blue","artist":"Miles Davis","artistId":"ar-1","year":1959,"songCount":5,"genre":"Jazz"}
|
|
]
|
|
}
|
|
}
|
|
}`))
|
|
case strings.HasSuffix(path, "/rest/getAlbum"):
|
|
w.Write([]byte(`{
|
|
"subsonic-response": {
|
|
"status": "ok",
|
|
"album": {
|
|
"song": [
|
|
{
|
|
"id":"song-1","title":"So What","artist":"Miles Davis",
|
|
"album":"Kind of Blue","year":1959,"track":1,"genre":"Jazz","duration":565
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}`))
|
|
case strings.HasSuffix(path, "/rest/search3"):
|
|
w.Write([]byte(`{
|
|
"subsonic-response": {
|
|
"status": "ok",
|
|
"searchResult3": {
|
|
"song": [
|
|
{
|
|
"id":"song-1","title":"So What","artist":"Miles Davis",
|
|
"album":"Kind of Blue","year":1959,"track":1,"genre":"Jazz","duration":565
|
|
},
|
|
{
|
|
"id":"song-7","title":"What'd I Say","artist":"Ray Charles",
|
|
"album":"What'd I Say","year":1959,"track":1,"genre":"Soul","duration":380
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}`))
|
|
case strings.HasSuffix(path, "/rest/scrobble"):
|
|
w.Write([]byte(`{"subsonic-response":{"status":"ok"}}`))
|
|
default:
|
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}
|
|
}
|
|
|
|
func newTestClient(t *testing.T) (*NavidromeClient, *httptest.Server) {
|
|
t.Helper()
|
|
srv := httptest.NewServer(subsonicHandler(t))
|
|
c := New(srv.URL, "testuser", "testpass")
|
|
return c, srv
|
|
}
|
|
|
|
func TestName(t *testing.T) {
|
|
c := New("http://localhost", "u", "p")
|
|
if c.Name() != "Navidrome" {
|
|
t.Errorf("Name() = %q, want %q", c.Name(), "Navidrome")
|
|
}
|
|
}
|
|
|
|
func TestPlaylists(t *testing.T) {
|
|
c, srv := newTestClient(t)
|
|
defer srv.Close()
|
|
|
|
lists, err := c.Playlists()
|
|
if err != nil {
|
|
t.Fatalf("Playlists() error: %v", err)
|
|
}
|
|
if len(lists) != 2 {
|
|
t.Fatalf("expected 2 playlists, got %d", len(lists))
|
|
}
|
|
if lists[0].ID != "pl-1" || lists[0].Name != "Jazz Classics" || lists[0].TrackCount != 12 {
|
|
t.Errorf("lists[0] = %+v", lists[0])
|
|
}
|
|
if lists[1].ID != "pl-2" || lists[1].Name != "Late Night" || lists[1].TrackCount != 8 {
|
|
t.Errorf("lists[1] = %+v", lists[1])
|
|
}
|
|
}
|
|
|
|
func TestPlaylists_Cached(t *testing.T) {
|
|
callCount := 0
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.HasSuffix(r.URL.Path, "/rest/getPlaylists") {
|
|
callCount++
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`{
|
|
"subsonic-response": {
|
|
"status": "ok",
|
|
"playlists": {"playlist": [{"id":"1","name":"P","songCount":1}]}
|
|
}
|
|
}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := New(srv.URL, "u", "p")
|
|
if _, err := c.Playlists(); err != nil {
|
|
t.Fatalf("first Playlists() error: %v", err)
|
|
}
|
|
if _, err := c.Playlists(); err != nil {
|
|
t.Fatalf("second Playlists() error: %v", err)
|
|
}
|
|
if callCount != 1 {
|
|
t.Errorf("expected getPlaylists called once, called %d times", callCount)
|
|
}
|
|
}
|
|
|
|
func TestTracks(t *testing.T) {
|
|
c, srv := newTestClient(t)
|
|
defer srv.Close()
|
|
|
|
tracks, err := c.Tracks("pl-1")
|
|
if err != nil {
|
|
t.Fatalf("Tracks() error: %v", err)
|
|
}
|
|
if len(tracks) != 2 {
|
|
t.Fatalf("expected 2 tracks, got %d", len(tracks))
|
|
}
|
|
|
|
tr := tracks[0]
|
|
if tr.Title != "So What" {
|
|
t.Errorf("Title = %q, want %q", tr.Title, "So What")
|
|
}
|
|
if tr.Artist != "Miles Davis" {
|
|
t.Errorf("Artist = %q, want %q", tr.Artist, "Miles Davis")
|
|
}
|
|
if tr.Album != "Kind of Blue" {
|
|
t.Errorf("Album = %q, want %q", tr.Album, "Kind of Blue")
|
|
}
|
|
if tr.Year != 1959 {
|
|
t.Errorf("Year = %d, want 1959", tr.Year)
|
|
}
|
|
if tr.TrackNumber != 1 {
|
|
t.Errorf("TrackNumber = %d, want 1", tr.TrackNumber)
|
|
}
|
|
if tr.Genre != "Jazz" {
|
|
t.Errorf("Genre = %q, want %q", tr.Genre, "Jazz")
|
|
}
|
|
if tr.DurationSecs != 565 {
|
|
t.Errorf("DurationSecs = %d, want 565", tr.DurationSecs)
|
|
}
|
|
if !tr.Stream {
|
|
t.Error("Stream = false, want true")
|
|
}
|
|
if got := tr.Meta(provider.MetaNavidromeID); got != "song-1" {
|
|
t.Errorf("Meta(NavidromeID) = %q, want %q", got, "song-1")
|
|
}
|
|
if !strings.Contains(tr.Path, "/rest/stream") {
|
|
t.Errorf("Path %q missing /rest/stream", tr.Path)
|
|
}
|
|
}
|
|
|
|
func TestTracks_Cached(t *testing.T) {
|
|
callCount := 0
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.HasSuffix(r.URL.Path, "/rest/getPlaylist") {
|
|
callCount++
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`{
|
|
"subsonic-response": {
|
|
"status": "ok",
|
|
"playlist": {"entry": [{"id":"1","title":"T","artist":"A","album":"Al","duration":60}]}
|
|
}
|
|
}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := New(srv.URL, "u", "p")
|
|
if _, err := c.Tracks("pl-1"); err != nil {
|
|
t.Fatalf("first Tracks() error: %v", err)
|
|
}
|
|
if _, err := c.Tracks("pl-1"); err != nil {
|
|
t.Fatalf("second Tracks() error: %v", err)
|
|
}
|
|
if callCount != 1 {
|
|
t.Errorf("expected getPlaylist called once, called %d times", callCount)
|
|
}
|
|
}
|
|
|
|
func TestArtists(t *testing.T) {
|
|
c, srv := newTestClient(t)
|
|
defer srv.Close()
|
|
|
|
artists, err := c.Artists()
|
|
if err != nil {
|
|
t.Fatalf("Artists() error: %v", err)
|
|
}
|
|
if len(artists) != 3 {
|
|
t.Fatalf("expected 3 artists (across 2 indexes), got %d", len(artists))
|
|
}
|
|
if artists[0].ID != "ar-1" || artists[0].Name != "Miles Davis" || artists[0].AlbumCount != 5 {
|
|
t.Errorf("artists[0] = %+v", artists[0])
|
|
}
|
|
if artists[2].ID != "ar-3" || artists[2].Name != "Thelonious Monk" {
|
|
t.Errorf("artists[2] = %+v", artists[2])
|
|
}
|
|
}
|
|
|
|
func TestArtistAlbums(t *testing.T) {
|
|
c, srv := newTestClient(t)
|
|
defer srv.Close()
|
|
|
|
albums, err := c.ArtistAlbums("ar-1")
|
|
if err != nil {
|
|
t.Fatalf("ArtistAlbums() error: %v", err)
|
|
}
|
|
if len(albums) != 2 {
|
|
t.Fatalf("expected 2 albums, got %d", len(albums))
|
|
}
|
|
if albums[0].ID != "al-1" || albums[0].Name != "Kind of Blue" || albums[0].Year != 1959 {
|
|
t.Errorf("albums[0] = %+v", albums[0])
|
|
}
|
|
if albums[1].ID != "al-2" || albums[1].Name != "Bitches Brew" || albums[1].Year != 1970 {
|
|
t.Errorf("albums[1] = %+v", albums[1])
|
|
}
|
|
}
|
|
|
|
func TestAlbumList(t *testing.T) {
|
|
c, srv := newTestClient(t)
|
|
defer srv.Close()
|
|
|
|
albums, err := c.AlbumList(SortAlphabeticalByName, 0, 50)
|
|
if err != nil {
|
|
t.Fatalf("AlbumList() error: %v", err)
|
|
}
|
|
if len(albums) != 1 {
|
|
t.Fatalf("expected 1 album, got %d", len(albums))
|
|
}
|
|
if albums[0].ID != "al-1" || albums[0].Name != "Kind of Blue" {
|
|
t.Errorf("albums[0] = %+v", albums[0])
|
|
}
|
|
}
|
|
|
|
func TestAlbumTracks(t *testing.T) {
|
|
c, srv := newTestClient(t)
|
|
defer srv.Close()
|
|
|
|
tracks, err := c.AlbumTracks("al-1")
|
|
if err != nil {
|
|
t.Fatalf("AlbumTracks() error: %v", err)
|
|
}
|
|
if len(tracks) != 1 {
|
|
t.Fatalf("expected 1 track, got %d", len(tracks))
|
|
}
|
|
if tracks[0].Title != "So What" || tracks[0].DurationSecs != 565 {
|
|
t.Errorf("tracks[0] = %+v", tracks[0])
|
|
}
|
|
}
|
|
|
|
func TestSearchTracks(t *testing.T) {
|
|
c, srv := newTestClient(t)
|
|
defer srv.Close()
|
|
|
|
tracks, err := c.SearchTracks(t.Context(), "what", 20)
|
|
if err != nil {
|
|
t.Fatalf("SearchTracks() error: %v", err)
|
|
}
|
|
if len(tracks) != 2 {
|
|
t.Fatalf("expected 2 tracks, got %d", len(tracks))
|
|
}
|
|
if tracks[0].Title != "So What" {
|
|
t.Errorf("tracks[0].Title = %q, want %q", tracks[0].Title, "So What")
|
|
}
|
|
if !tracks[0].Stream {
|
|
t.Error("tracks[0].Stream should be true")
|
|
}
|
|
if tracks[0].Meta(provider.MetaNavidromeID) != "song-1" {
|
|
t.Errorf("tracks[0] meta navidrome id = %q, want song-1", tracks[0].Meta(provider.MetaNavidromeID))
|
|
}
|
|
}
|
|
|
|
func TestSubsonicError(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`{
|
|
"subsonic-response": {
|
|
"status": "failed",
|
|
"error": {"code": 40, "message": "Wrong username or password"}
|
|
}
|
|
}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := New(srv.URL, "bad", "creds")
|
|
_, err := c.Playlists()
|
|
if err == nil {
|
|
t.Fatal("expected error for bad credentials, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "Wrong username or password") {
|
|
t.Errorf("error = %q, expected credential error", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestHTTPError(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := New(srv.URL, "u", "p")
|
|
_, err := c.Playlists()
|
|
if err == nil {
|
|
t.Fatal("expected error for HTTP 500, got nil")
|
|
}
|
|
}
|
|
|
|
func TestBuildURL_AuthParams(t *testing.T) {
|
|
c := New("https://music.example.com", "alice", "secret123")
|
|
u := c.buildURL("ping", nil)
|
|
|
|
parsed, err := url.Parse(u)
|
|
if err != nil {
|
|
t.Fatalf("buildURL() returned invalid URL: %v", err)
|
|
}
|
|
q := parsed.Query()
|
|
if q.Get("u") != "alice" {
|
|
t.Errorf("user = %q, want alice", q.Get("u"))
|
|
}
|
|
if q.Get("t") == "" {
|
|
t.Error("token (t) is empty")
|
|
}
|
|
if q.Get("s") == "" {
|
|
t.Error("salt (s) is empty")
|
|
}
|
|
if q.Get("v") != "1.0.0" {
|
|
t.Errorf("version = %q, want 1.0.0", q.Get("v"))
|
|
}
|
|
if q.Get("c") != "cliamp" {
|
|
t.Errorf("client = %q, want cliamp", q.Get("c"))
|
|
}
|
|
if q.Get("f") != "json" {
|
|
t.Errorf("format = %q, want json", q.Get("f"))
|
|
}
|
|
if !strings.HasPrefix(u, "https://music.example.com/rest/ping?") {
|
|
t.Errorf("URL = %q, missing expected prefix", u)
|
|
}
|
|
}
|
|
|
|
func TestIsSubsonicStreamURL(t *testing.T) {
|
|
tests := []struct {
|
|
url string
|
|
want bool
|
|
}{
|
|
{"https://music.example.com/rest/stream?id=1", true},
|
|
{"https://music.example.com/rest/stream.view?id=1", true},
|
|
{"https://music.example.com/rest/download?id=1", true},
|
|
{"https://music.example.com/rest/download.view?id=1", true},
|
|
{"https://music.example.com/rest/getPlaylists", false},
|
|
{"https://example.com/song.mp3", false},
|
|
{"", false},
|
|
}
|
|
for _, tt := range tests {
|
|
if got := IsSubsonicStreamURL(tt.url); got != tt.want {
|
|
t.Errorf("IsSubsonicStreamURL(%q) = %v, want %v", tt.url, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNewFromEnv(t *testing.T) {
|
|
t.Setenv("NAVIDROME_URL", "")
|
|
t.Setenv("NAVIDROME_USER", "")
|
|
t.Setenv("NAVIDROME_PASS", "")
|
|
if c := NewFromEnv(); c != nil {
|
|
t.Error("NewFromEnv() should return nil when env vars are empty")
|
|
}
|
|
|
|
t.Setenv("NAVIDROME_URL", "https://music.test")
|
|
t.Setenv("NAVIDROME_USER", "alice")
|
|
t.Setenv("NAVIDROME_PASS", "secret")
|
|
c := NewFromEnv()
|
|
if c == nil {
|
|
t.Fatal("NewFromEnv() returned nil with all env vars set")
|
|
}
|
|
if c.url != "https://music.test" || c.user != "alice" || c.password != "secret" {
|
|
t.Errorf("NewFromEnv() = %+v", c)
|
|
}
|
|
}
|
|
|
|
func TestNewFromConfig(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
cfg config.NavidromeConfig
|
|
wantNil bool
|
|
}{
|
|
{"empty", config.NavidromeConfig{}, true},
|
|
{"no password", config.NavidromeConfig{URL: "http://localhost", User: "u"}, true},
|
|
{"no url", config.NavidromeConfig{User: "u", Password: "p"}, true},
|
|
{"valid", config.NavidromeConfig{URL: "http://localhost", User: "u", Password: "p"}, false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := NewFromConfig(tt.cfg)
|
|
if (c == nil) != tt.wantNil {
|
|
t.Errorf("NewFromConfig(%+v) nil=%v, want nil=%v", tt.cfg, c == nil, tt.wantNil)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewFromConfig_BrowseSort(t *testing.T) {
|
|
cfg := config.NavidromeConfig{
|
|
URL: "http://localhost", User: "u", Password: "p",
|
|
BrowseSort: SortNewest,
|
|
}
|
|
c := NewFromConfig(cfg)
|
|
if c.browseSort != SortNewest {
|
|
t.Errorf("browseSort = %q, want %q", c.browseSort, SortNewest)
|
|
}
|
|
}
|
|
|
|
func TestNewFromConfig_ScrobbleDisabled(t *testing.T) {
|
|
cfg := config.NavidromeConfig{
|
|
URL: "http://localhost", User: "u", Password: "p",
|
|
ScrobbleDisabled: true,
|
|
}
|
|
c := NewFromConfig(cfg)
|
|
if !c.scrobbleDisabled {
|
|
t.Error("scrobbleDisabled = false, want true")
|
|
}
|
|
}
|
|
|
|
func TestAlbumSortTypes(t *testing.T) {
|
|
c := New("http://localhost", "u", "p")
|
|
sorts := c.AlbumSortTypes()
|
|
if len(sorts) != 8 {
|
|
t.Fatalf("expected 8 sort types, got %d", len(sorts))
|
|
}
|
|
}
|
|
|
|
func TestDefaultAlbumSort(t *testing.T) {
|
|
c := New("http://localhost", "u", "p")
|
|
if c.DefaultAlbumSort() != SortAlphabeticalByName {
|
|
t.Errorf("DefaultAlbumSort() = %q, want %q", c.DefaultAlbumSort(), SortAlphabeticalByName)
|
|
}
|
|
c.browseSort = SortNewest
|
|
if c.DefaultAlbumSort() != SortNewest {
|
|
t.Errorf("DefaultAlbumSort() = %q, want %q", c.DefaultAlbumSort(), SortNewest)
|
|
}
|
|
}
|
|
|
|
func TestCanReportPlayback(t *testing.T) {
|
|
c := New("http://localhost", "u", "p")
|
|
track := trackWithNavidromeMeta("song-1")
|
|
if !c.CanReportPlayback(track) {
|
|
t.Error("CanReportPlayback() = false for track with navidrome ID")
|
|
}
|
|
|
|
c.scrobbleDisabled = true
|
|
if c.CanReportPlayback(track) {
|
|
t.Error("CanReportPlayback() = true when scrobbling is disabled")
|
|
}
|
|
}
|
|
|
|
func TestCanReportPlayback_NoMeta(t *testing.T) {
|
|
c := New("http://localhost", "u", "p")
|
|
track := trackWithNavidromeMeta("")
|
|
if c.CanReportPlayback(track) {
|
|
t.Error("CanReportPlayback() = true for track without navidrome ID")
|
|
}
|
|
}
|
|
|
|
func TestScrobble(t *testing.T) {
|
|
var gotSubmission string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.HasSuffix(r.URL.Path, "/rest/scrobble") {
|
|
gotSubmission = r.URL.Query().Get("submission")
|
|
}
|
|
w.Write([]byte(`{"subsonic-response":{"status":"ok"}}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := New(srv.URL, "u", "p")
|
|
|
|
c.ReportNowPlaying(trackWithNavidromeMeta("song-1"), 0, false)
|
|
if gotSubmission != "false" {
|
|
t.Errorf("ReportNowPlaying submission = %q, want false", gotSubmission)
|
|
}
|
|
|
|
c.ReportScrobble(trackWithNavidromeMeta("song-1"), 0, 0, false)
|
|
if gotSubmission != "true" {
|
|
t.Errorf("ReportScrobble submission = %q, want true", gotSubmission)
|
|
}
|
|
}
|
|
|
|
func TestCheckSubsonicError(t *testing.T) {
|
|
if err := checkSubsonicError("ok", nil); err != nil {
|
|
t.Errorf("expected nil for status ok, got %v", err)
|
|
}
|
|
if err := checkSubsonicError("", nil); err != nil {
|
|
t.Errorf("expected nil for empty status, got %v", err)
|
|
}
|
|
|
|
err := checkSubsonicError("failed", &subsonicError{Code: 40, Message: "Wrong password"})
|
|
if err == nil {
|
|
t.Fatal("expected error for failed status")
|
|
}
|
|
if !strings.Contains(err.Error(), "Wrong password") {
|
|
t.Errorf("error = %q, missing message", err.Error())
|
|
}
|
|
|
|
err = checkSubsonicError("failed", nil)
|
|
if err == nil {
|
|
t.Fatal("expected error for failed status with nil error body")
|
|
}
|
|
}
|
|
|
|
func trackWithNavidromeMeta(id string) playlist.Track {
|
|
meta := map[string]string{}
|
|
if id != "" {
|
|
meta[provider.MetaNavidromeID] = id
|
|
}
|
|
return playlist.Track{ProviderMeta: meta}
|
|
}
|