Files

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)
}
}