258 lines
7.5 KiB
Go
258 lines
7.5 KiB
Go
package subsonic
|
|
|
|
import (
|
|
"context"
|
|
"encoding/xml"
|
|
"net/http/httptest"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.noroutes.net/NoRoutes/vibetunes/internal/database"
|
|
)
|
|
|
|
// seedLibrary scans a small real (ffmpeg-generated) library into s so
|
|
// browsing endpoints have real rows to return. Skips the test if
|
|
// ffmpeg/ffprobe aren't installed.
|
|
func seedLibrary(t *testing.T, s *Server) (folder *database.MusicFolder) {
|
|
t.Helper()
|
|
if _, err := exec.LookPath("ffmpeg"); err != nil {
|
|
t.Skip("ffmpeg not installed")
|
|
}
|
|
if _, err := exec.LookPath("ffprobe"); err != nil {
|
|
t.Skip("ffprobe not installed")
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
songDir := filepath.Join(dir, "Test Artist", "Test Album")
|
|
if err := os.MkdirAll(songDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for i, title := range []string{"First Song", "Second Song"} {
|
|
out := filepath.Join(songDir, title+".mp3")
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
cmd := exec.CommandContext(ctx, "ffmpeg",
|
|
"-f", "lavfi", "-i", "sine=frequency=440:duration=1",
|
|
"-metadata", "title="+title,
|
|
"-metadata", "artist=Test Artist",
|
|
"-metadata", "album=Test Album",
|
|
"-metadata", "genre=Rock",
|
|
"-metadata", "track="+strconv.Itoa(i+1),
|
|
"-metadata", "date=2021",
|
|
"-codec:a", "libmp3lame", "-b:a", "64k",
|
|
out)
|
|
if combined, err := cmd.CombinedOutput(); err != nil {
|
|
cancel()
|
|
t.Fatalf("ffmpeg: %v\n%s", err, combined)
|
|
}
|
|
cancel()
|
|
}
|
|
|
|
f, err := database.CreateMusicFolder(s.DB, "Test Music", dir, "music")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.Scanner.ScanFolder(context.Background(), f.ID); err != nil {
|
|
t.Fatalf("ScanFolder: %v", err)
|
|
}
|
|
return f
|
|
}
|
|
|
|
func TestGetMusicFoldersAndGenresAndArtists(t *testing.T) {
|
|
s, srv := newTestRecorderServer(t)
|
|
createTestUser(t, s, "alice", "password123", false)
|
|
seedLibrary(t, s)
|
|
|
|
q := baseParams("alice", "password123")
|
|
q.Set("f", "json")
|
|
|
|
resp := getPath(t, srv.URL, "/rest/getMusicFolders.view", q)
|
|
j := decodeJSON(t, resp)
|
|
folders := j["musicFolders"].(map[string]any)["musicFolder"].([]any)
|
|
if len(folders) != 1 {
|
|
t.Fatalf("musicFolders = %v, want 1", folders)
|
|
}
|
|
|
|
resp = getPath(t, srv.URL, "/rest/getGenres.view", q)
|
|
j = decodeJSON(t, resp)
|
|
genres := j["genres"].(map[string]any)["genre"].([]any)
|
|
if len(genres) != 1 {
|
|
t.Fatalf("genres = %v, want 1", genres)
|
|
}
|
|
|
|
resp = getPath(t, srv.URL, "/rest/getArtists.view", q)
|
|
j = decodeJSON(t, resp)
|
|
indexes := j["artists"].(map[string]any)["index"].([]any)
|
|
if len(indexes) == 0 {
|
|
t.Fatalf("artists index = %v, want at least 1 letter group", indexes)
|
|
}
|
|
}
|
|
|
|
func TestGetArtistAndAlbumAndSong(t *testing.T) {
|
|
s, db := newTestServer(t)
|
|
createTestUser(t, s, "alice", "password123", false)
|
|
seedLibrary(t, s)
|
|
srv := httptest.NewServer(s.Router())
|
|
defer srv.Close()
|
|
|
|
artists, err := database.ListArtists(db)
|
|
if err != nil || len(artists) == 0 {
|
|
t.Fatalf("ListArtists: %v %v", artists, err)
|
|
}
|
|
artistID := artists[0].ID
|
|
|
|
q := baseParams("alice", "password123")
|
|
q.Set("f", "json")
|
|
q.Set("id", artistID)
|
|
resp := getPath(t, srv.URL, "/rest/getArtist.view", q)
|
|
j := decodeJSON(t, resp)
|
|
artist := j["artist"].(map[string]any)
|
|
albums := artist["album"].([]any)
|
|
if len(albums) != 1 {
|
|
t.Fatalf("albums = %v, want 1", albums)
|
|
}
|
|
albumID := albums[0].(map[string]any)["id"].(string)
|
|
|
|
q.Set("id", albumID)
|
|
resp = getPath(t, srv.URL, "/rest/getAlbum.view", q)
|
|
j = decodeJSON(t, resp)
|
|
album := j["album"].(map[string]any)
|
|
songs := album["song"].([]any)
|
|
if len(songs) != 2 {
|
|
t.Fatalf("songs = %v, want 2", songs)
|
|
}
|
|
songID := songs[0].(map[string]any)["id"].(string)
|
|
|
|
q.Set("id", songID)
|
|
resp = getPath(t, srv.URL, "/rest/getSong.view", q)
|
|
j = decodeJSON(t, resp)
|
|
song := j["song"].(map[string]any)
|
|
if song["title"] == nil {
|
|
t.Fatalf("song missing title: %v", song)
|
|
}
|
|
}
|
|
|
|
func TestGetAlbumNotFound(t *testing.T) {
|
|
s, srv := newTestRecorderServer(t)
|
|
createTestUser(t, s, "alice", "password123", false)
|
|
|
|
q := baseParams("alice", "password123")
|
|
q.Set("id", "al_doesnotexist")
|
|
resp := getPath(t, srv.URL, "/rest/getAlbum.view", q)
|
|
x := decodeXML(t, resp)
|
|
if x.Status != "failed" || x.Error == nil || x.Error.Code != ErrNotFound {
|
|
t.Fatalf("expected error 70, got %+v", x)
|
|
}
|
|
}
|
|
|
|
func TestGetAlbumMissingID(t *testing.T) {
|
|
s, srv := newTestRecorderServer(t)
|
|
createTestUser(t, s, "alice", "password123", false)
|
|
|
|
resp := getPath(t, srv.URL, "/rest/getAlbum.view", baseParams("alice", "password123"))
|
|
x := decodeXML(t, resp)
|
|
if x.Status != "failed" || x.Error == nil || x.Error.Code != ErrMissingParameter {
|
|
t.Fatalf("expected error 10, got %+v", x)
|
|
}
|
|
}
|
|
|
|
func TestGetIndexesAndMusicDirectory(t *testing.T) {
|
|
s, srv := newTestRecorderServer(t)
|
|
createTestUser(t, s, "alice", "password123", false)
|
|
seedLibrary(t, s)
|
|
|
|
q := baseParams("alice", "password123")
|
|
resp := getPath(t, srv.URL, "/rest/getIndexes.view", q)
|
|
defer resp.Body.Close()
|
|
var out struct {
|
|
Indexes struct {
|
|
Index []struct {
|
|
Artist []struct {
|
|
ID string `xml:"id,attr"`
|
|
Name string `xml:"name,attr"`
|
|
} `xml:"artist"`
|
|
} `xml:"index"`
|
|
} `xml:"indexes"`
|
|
}
|
|
if err := xml.NewDecoder(resp.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if len(out.Indexes.Index) == 0 || len(out.Indexes.Index[0].Artist) == 0 {
|
|
t.Fatalf("expected at least one artist directory, got %+v", out)
|
|
}
|
|
dirID := out.Indexes.Index[0].Artist[0].ID
|
|
|
|
q2 := baseParams("alice", "password123")
|
|
q2.Set("id", dirID)
|
|
resp2 := getPath(t, srv.URL, "/rest/getMusicDirectory.view", q2)
|
|
defer resp2.Body.Close()
|
|
var dirOut struct {
|
|
Directory struct {
|
|
Child []struct {
|
|
IsDir bool `xml:"isDir,attr"`
|
|
Title string `xml:"title,attr"`
|
|
} `xml:"child"`
|
|
} `xml:"directory"`
|
|
}
|
|
if err := xml.NewDecoder(resp2.Body).Decode(&dirOut); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if len(dirOut.Directory.Child) != 1 || !dirOut.Directory.Child[0].IsDir {
|
|
t.Fatalf("expected one subdirectory (the album), got %+v", dirOut)
|
|
}
|
|
}
|
|
|
|
func TestGetMusicDirectoryUnauthorizedFolder(t *testing.T) {
|
|
s, db := newTestServer(t)
|
|
u := createTestUser(t, s, "alice", "password123", false)
|
|
folder := seedLibrary(t, s)
|
|
srv := httptest.NewServer(s.Router())
|
|
defer srv.Close()
|
|
|
|
// Restrict alice to a different (nonexistent) folder set, i.e. no access to `folder`.
|
|
other, err := database.CreateMusicFolder(db, "Other", t.TempDir(), "music")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := database.SetAllowedMusicFolders(db, u.ID, []string{other.ID}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
q := baseParams("alice", "password123")
|
|
q.Set("id", encodeDirID(folder.ID, "Test Artist"))
|
|
resp := getPath(t, srv.URL, "/rest/getMusicDirectory.view", q)
|
|
x := decodeXML(t, resp)
|
|
if x.Status != "failed" || x.Error == nil || x.Error.Code != ErrUnauthorized {
|
|
t.Fatalf("expected error 50 for restricted folder, got %+v", x)
|
|
}
|
|
}
|
|
|
|
func TestGetVideosEmpty(t *testing.T) {
|
|
s, srv := newTestRecorderServer(t)
|
|
createTestUser(t, s, "alice", "password123", false)
|
|
|
|
q := baseParams("alice", "password123")
|
|
q.Set("f", "json")
|
|
resp := getPath(t, srv.URL, "/rest/getVideos.view", q)
|
|
j := decodeJSON(t, resp)
|
|
if j["status"] != "ok" {
|
|
t.Fatalf("getVideos status = %v", j["status"])
|
|
}
|
|
}
|
|
|
|
func TestGetTopSongsUnknownArtist(t *testing.T) {
|
|
s, srv := newTestRecorderServer(t)
|
|
createTestUser(t, s, "alice", "password123", false)
|
|
|
|
q := baseParams("alice", "password123")
|
|
q.Set("artist", "Nonexistent Artist XYZ")
|
|
resp := getPath(t, srv.URL, "/rest/getTopSongs.view", q)
|
|
x := decodeXML(t, resp)
|
|
if x.Status != "ok" {
|
|
t.Fatalf("getTopSongs for unknown artist should return ok with empty list, got %+v", x)
|
|
}
|
|
}
|