338 lines
10 KiB
Go
338 lines
10 KiB
Go
package plex
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/bjarneo/cliamp/config"
|
|
)
|
|
|
|
// sectionsHandler returns a handler that serves a single music section.
|
|
func sectionsHandler(t *testing.T) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
switch {
|
|
case strings.HasSuffix(r.URL.Path, "/library/sections"):
|
|
w.Write([]byte(`{"MediaContainer":{"Directory":[{"key":"3","type":"artist","title":"Music"}]}}`))
|
|
case strings.HasSuffix(r.URL.Path, "/library/sections/3/all"):
|
|
w.Write([]byte(`{
|
|
"MediaContainer": {
|
|
"totalSize": 2,
|
|
"Metadata": [
|
|
{"ratingKey":"100","title":"Kind of Blue","parentTitle":"Miles Davis","year":1959,"leafCount":5},
|
|
{"ratingKey":"101","title":"Bitches Brew","parentTitle":"Miles Davis","year":1970,"leafCount":4}
|
|
]
|
|
}
|
|
}`))
|
|
case strings.Contains(r.URL.Path, "/library/metadata/100/children"):
|
|
w.Write([]byte(`{
|
|
"MediaContainer": {
|
|
"Metadata": [
|
|
{
|
|
"ratingKey":"200","title":"So What","grandparentTitle":"Miles Davis",
|
|
"parentTitle":"Kind of Blue","year":1959,"index":1,"duration":565000,
|
|
"Media":[{"Part":[{"key":"/library/parts/1/111/SoWhat.flac"}]}]
|
|
}
|
|
]
|
|
}
|
|
}`))
|
|
case strings.Contains(r.URL.Path, "/library/metadata/101/children"):
|
|
w.Write([]byte(`{
|
|
"MediaContainer": {
|
|
"Metadata": [
|
|
{
|
|
"ratingKey":"201","title":"Pharaoh's Dance","grandparentTitle":"Miles Davis",
|
|
"parentTitle":"Bitches Brew","year":1970,"index":1,"duration":1140000,
|
|
"Media":[{"Part":[{"key":"/library/parts/2/222/PharaohsDance.flac"}]}]
|
|
}
|
|
]
|
|
}
|
|
}`))
|
|
default:
|
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestProvider_Name(t *testing.T) {
|
|
p := newProvider(NewClient("http://localhost:32400", "tok"))
|
|
if p.Name() != "Plex" {
|
|
t.Errorf("Name() = %q, want %q", p.Name(), "Plex")
|
|
}
|
|
}
|
|
|
|
func TestProvider_Playlists(t *testing.T) {
|
|
srv := httptest.NewServer(sectionsHandler(t))
|
|
defer srv.Close()
|
|
|
|
p := newProvider(NewClient(srv.URL, "tok"))
|
|
lists, err := p.Playlists()
|
|
if err != nil {
|
|
t.Fatalf("Playlists() error: %v", err)
|
|
}
|
|
if len(lists) != 2 {
|
|
t.Fatalf("expected 2 playlists, got %d", len(lists))
|
|
}
|
|
|
|
// Check first album entry
|
|
if lists[0].ID != "100" {
|
|
t.Errorf("lists[0].ID = %q, want %q", lists[0].ID, "100")
|
|
}
|
|
if !strings.Contains(lists[0].Name, "Miles Davis") {
|
|
t.Errorf("lists[0].Name %q missing artist", lists[0].Name)
|
|
}
|
|
if !strings.Contains(lists[0].Name, "Kind of Blue") {
|
|
t.Errorf("lists[0].Name %q missing album title", lists[0].Name)
|
|
}
|
|
if !strings.Contains(lists[0].Name, "1959") {
|
|
t.Errorf("lists[0].Name %q missing year", lists[0].Name)
|
|
}
|
|
if lists[0].TrackCount != 5 {
|
|
t.Errorf("lists[0].TrackCount = %d, want 5", lists[0].TrackCount)
|
|
}
|
|
}
|
|
|
|
func TestProvider_Playlists_Cached(t *testing.T) {
|
|
callCount := 0
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.HasSuffix(r.URL.Path, "/library/sections") {
|
|
callCount++
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
switch {
|
|
case strings.HasSuffix(r.URL.Path, "/library/sections"):
|
|
w.Write([]byte(`{"MediaContainer":{"Directory":[{"key":"3","type":"artist","title":"Music"}]}}`))
|
|
case strings.HasSuffix(r.URL.Path, "/library/sections/3/all"):
|
|
w.Write([]byte(`{"MediaContainer":{"totalSize":1,"Metadata":[{"ratingKey":"1","title":"Album","parentTitle":"Artist","year":2020,"leafCount":1}]}}`))
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := newProvider(NewClient(srv.URL, "tok"))
|
|
if _, err := p.Playlists(); err != nil {
|
|
t.Fatalf("first Playlists() error: %v", err)
|
|
}
|
|
if _, err := p.Playlists(); err != nil {
|
|
t.Fatalf("second Playlists() error: %v", err)
|
|
}
|
|
if callCount != 1 {
|
|
t.Errorf("expected /library/sections called once, called %d times", callCount)
|
|
}
|
|
}
|
|
|
|
func TestProvider_Playlists_NoMusicSections(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(`{"MediaContainer":{"Directory":[{"key":"1","type":"movie","title":"Movies"}]}}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := newProvider(NewClient(srv.URL, "tok"))
|
|
_, err := p.Playlists()
|
|
if err == nil {
|
|
t.Fatal("expected error for no music sections, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "no matching music libraries") {
|
|
t.Errorf("unexpected error message: %q", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestProvider_Tracks(t *testing.T) {
|
|
srv := httptest.NewServer(sectionsHandler(t))
|
|
defer srv.Close()
|
|
|
|
p := newProvider(NewClient(srv.URL, "tok"))
|
|
tracks, err := p.Tracks("100")
|
|
if err != nil {
|
|
t.Fatalf("Tracks() error: %v", err)
|
|
}
|
|
if len(tracks) != 1 {
|
|
t.Fatalf("expected 1 track, 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.DurationSecs != 565 {
|
|
t.Errorf("DurationSecs = %d, want 565", tr.DurationSecs)
|
|
}
|
|
if !tr.Stream {
|
|
t.Error("Stream = false, want true")
|
|
}
|
|
if !strings.HasPrefix(tr.Path, srv.URL) {
|
|
t.Errorf("Path %q does not start with server URL", tr.Path)
|
|
}
|
|
if !strings.Contains(tr.Path, "X-Plex-Token=tok") {
|
|
t.Errorf("Path %q missing X-Plex-Token", tr.Path)
|
|
}
|
|
if !strings.Contains(tr.Path, "/library/parts/1/111/SoWhat.flac") {
|
|
t.Errorf("Path %q missing part key", tr.Path)
|
|
}
|
|
}
|
|
|
|
func TestProvider_Tracks_SkipsMissingPart(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(`{
|
|
"MediaContainer": {
|
|
"Metadata": [
|
|
{"ratingKey":"1","title":"Has Part","Media":[{"Part":[{"key":"/library/parts/1/1/file.mp3"}]}]},
|
|
{"ratingKey":"2","title":"No Part","Media":[]},
|
|
{"ratingKey":"3","title":"Also No Part"}
|
|
]
|
|
}
|
|
}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := newProvider(NewClient(srv.URL, "tok"))
|
|
tracks, err := p.Tracks("42")
|
|
if err != nil {
|
|
t.Fatalf("Tracks() error: %v", err)
|
|
}
|
|
if len(tracks) != 1 {
|
|
t.Errorf("expected 1 track (skipping those with no part), got %d", len(tracks))
|
|
}
|
|
if tracks[0].Title != "Has Part" {
|
|
t.Errorf("expected 'Has Part', got %q", tracks[0].Title)
|
|
}
|
|
}
|
|
|
|
func TestProvider_Tracks_Cached(t *testing.T) {
|
|
callCount := 0
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
callCount++
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`{"MediaContainer":{"Metadata":[{"ratingKey":"1","title":"T","Media":[{"Part":[{"key":"/p/1/1/f.mp3"}]}]}]}}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := newProvider(NewClient(srv.URL, "tok"))
|
|
if _, err := p.Tracks("99"); err != nil {
|
|
t.Fatalf("first Tracks() error: %v", err)
|
|
}
|
|
if _, err := p.Tracks("99"); err != nil {
|
|
t.Fatalf("second Tracks() error: %v", err)
|
|
}
|
|
if callCount != 1 {
|
|
t.Errorf("expected children endpoint called once, called %d times", callCount)
|
|
}
|
|
}
|
|
|
|
func TestProvider_Tracks_ClientError(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := newProvider(NewClient(srv.URL, "bad-token"))
|
|
_, err := p.Tracks("42")
|
|
if err == nil {
|
|
t.Fatal("expected error on 401, got nil")
|
|
}
|
|
}
|
|
|
|
func TestNewFromConfig_NilWhenMissing(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
cfg config.PlexConfig
|
|
}{
|
|
{"empty", config.PlexConfig{}},
|
|
{"no token", config.PlexConfig{URL: "http://localhost:32400"}},
|
|
{"no url", config.PlexConfig{Token: "tok"}},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if p := NewFromConfig(tt.cfg); p != nil {
|
|
t.Errorf("NewFromConfig(%+v) = non-nil, want nil", tt.cfg)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewFromConfig_OK(t *testing.T) {
|
|
cfg := config.PlexConfig{URL: "http://localhost:32400", Token: "mytoken"}
|
|
if p := NewFromConfig(cfg); p == nil {
|
|
t.Error("NewFromConfig() returned nil for valid config")
|
|
}
|
|
}
|
|
|
|
func TestNewFromConfig_LibrariesWired(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
libraries []string
|
|
wantLen int
|
|
}{
|
|
{"no filter returns all", nil, 2},
|
|
{"filter to Jazz", []string{"Jazz"}, 1},
|
|
{"filter to jazz case-insensitive", []string{"jazz"}, 1},
|
|
{"filter to both", []string{"Music", "Jazz"}, 2},
|
|
{"no match returns error", []string{"Classical"}, 0},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
switch {
|
|
case strings.HasSuffix(r.URL.Path, "/library/sections"):
|
|
w.Write([]byte(`{"MediaContainer":{"Directory":[
|
|
{"key":"1","type":"artist","title":"Music"},
|
|
{"key":"2","type":"artist","title":"Jazz"}
|
|
]}}`))
|
|
case strings.HasSuffix(r.URL.Path, "/library/sections/1/all"):
|
|
w.Write([]byte(`{"MediaContainer":{"totalSize":1,"Metadata":[
|
|
{"ratingKey":"11","title":"Bitches Brew","parentTitle":"Miles Davis","year":1970,"leafCount":4}
|
|
]}}`))
|
|
case strings.HasSuffix(r.URL.Path, "/library/sections/2/all"):
|
|
w.Write([]byte(`{"MediaContainer":{"totalSize":1,"Metadata":[
|
|
{"ratingKey":"10","title":"Kind of Blue","parentTitle":"Miles Davis","year":1959,"leafCount":5}
|
|
]}}`))
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
cfg := config.PlexConfig{
|
|
URL: srv.URL,
|
|
Token: "tok",
|
|
Libraries: tt.libraries,
|
|
}
|
|
p := NewFromConfig(cfg)
|
|
if p == nil {
|
|
t.Fatal("NewFromConfig() returned nil for valid config")
|
|
}
|
|
|
|
lists, err := p.Playlists()
|
|
if tt.wantLen == 0 {
|
|
if err == nil {
|
|
t.Errorf("expected error for no matching libraries, got %d playlists", len(lists))
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("Playlists() error: %v", err)
|
|
}
|
|
if len(lists) != tt.wantLen {
|
|
t.Fatalf("got %d playlists, want %d", len(lists), tt.wantLen)
|
|
}
|
|
})
|
|
}
|
|
}
|