mirror of
https://github.com/navidrome/navidrome.git
synced 2026-02-08 05:51:06 -05:00
Compare commits
5 Commits
master
...
subsonic-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6a3ef6ea5 | ||
|
|
f67324278e | ||
|
|
fc8b7283f0 | ||
|
|
777638e84d | ||
|
|
ebe3c1d06c |
354
server/e2e/e2e_suite_test.go
Normal file
354
server/e2e/e2e_suite_test.go
Normal file
@@ -0,0 +1,354 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/conf/configtest"
|
||||
"github.com/navidrome/navidrome/core"
|
||||
"github.com/navidrome/navidrome/core/artwork"
|
||||
"github.com/navidrome/navidrome/core/auth"
|
||||
"github.com/navidrome/navidrome/core/external"
|
||||
"github.com/navidrome/navidrome/core/metrics"
|
||||
"github.com/navidrome/navidrome/core/playback"
|
||||
"github.com/navidrome/navidrome/core/scrobbler"
|
||||
"github.com/navidrome/navidrome/core/storage/storagetest"
|
||||
"github.com/navidrome/navidrome/db"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/persistence"
|
||||
"github.com/navidrome/navidrome/scanner"
|
||||
"github.com/navidrome/navidrome/server/events"
|
||||
"github.com/navidrome/navidrome/server/subsonic"
|
||||
"github.com/navidrome/navidrome/tests"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestSubsonicE2E(t *testing.T) {
|
||||
tests.Init(t, false)
|
||||
defer db.Close(t.Context())
|
||||
log.SetLevel(log.LevelFatal)
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Subsonic API E2E Suite")
|
||||
}
|
||||
|
||||
// Easy aliases for the storagetest package
|
||||
type _t = map[string]any
|
||||
|
||||
var template = storagetest.Template
|
||||
var track = storagetest.Track
|
||||
|
||||
// Shared test state
|
||||
var (
|
||||
ctx context.Context
|
||||
ds *tests.MockDataStore
|
||||
router *subsonic.Router
|
||||
lib model.Library
|
||||
|
||||
// Snapshot paths for fast DB restore
|
||||
dbFilePath string
|
||||
snapshotPath string
|
||||
|
||||
// Admin user used for most tests
|
||||
adminUser = model.User{
|
||||
ID: "admin-1",
|
||||
UserName: "admin",
|
||||
Name: "Admin User",
|
||||
IsAdmin: true,
|
||||
}
|
||||
)
|
||||
|
||||
func createFS(files fstest.MapFS) storagetest.FakeFS {
|
||||
fs := storagetest.FakeFS{}
|
||||
fs.SetFiles(files)
|
||||
storagetest.Register("fake", &fs)
|
||||
return fs
|
||||
}
|
||||
|
||||
// buildTestFS creates the full test filesystem matching the plan
|
||||
func buildTestFS() storagetest.FakeFS {
|
||||
abbeyRoad := template(_t{"albumartist": "The Beatles", "artist": "The Beatles", "album": "Abbey Road", "year": 1969, "genre": "Rock"})
|
||||
help := template(_t{"albumartist": "The Beatles", "artist": "The Beatles", "album": "Help!", "year": 1965, "genre": "Rock"})
|
||||
ledZepIV := template(_t{"albumartist": "Led Zeppelin", "artist": "Led Zeppelin", "album": "IV", "year": 1971, "genre": "Rock"})
|
||||
kindOfBlue := template(_t{"albumartist": "Miles Davis", "artist": "Miles Davis", "album": "Kind of Blue", "year": 1959, "genre": "Jazz"})
|
||||
popTrack := template(_t{"albumartist": "Various", "artist": "Various", "album": "Pop", "year": 2020, "genre": "Pop"})
|
||||
|
||||
return createFS(fstest.MapFS{
|
||||
// Rock / The Beatles / Abbey Road
|
||||
"Rock/The Beatles/Abbey Road/01 - Come Together.mp3": abbeyRoad(track(1, "Come Together")),
|
||||
"Rock/The Beatles/Abbey Road/02 - Something.mp3": abbeyRoad(track(2, "Something")),
|
||||
// Rock / The Beatles / Help!
|
||||
"Rock/The Beatles/Help!/01 - Help.mp3": help(track(1, "Help!")),
|
||||
// Rock / Led Zeppelin / IV
|
||||
"Rock/Led Zeppelin/IV/01 - Stairway To Heaven.mp3": ledZepIV(track(1, "Stairway To Heaven")),
|
||||
// Jazz / Miles Davis / Kind of Blue
|
||||
"Jazz/Miles Davis/Kind of Blue/01 - So What.mp3": kindOfBlue(track(1, "So What")),
|
||||
// Pop (standalone track)
|
||||
"Pop/01 - Standalone Track.mp3": popTrack(track(1, "Standalone Track")),
|
||||
// _empty folder (directory with no audio)
|
||||
"_empty/.keep": &fstest.MapFile{Data: []byte{}, ModTime: time.Now()},
|
||||
})
|
||||
}
|
||||
|
||||
// newReq creates an authenticated GET request for the given endpoint with optional query parameters.
|
||||
// Parameters are provided as key-value pairs: newReq("getAlbum", "id", "123")
|
||||
func newReq(endpoint string, params ...string) *http.Request {
|
||||
return newReqWithUser(adminUser, endpoint, params...)
|
||||
}
|
||||
|
||||
// newReqWithUser creates an authenticated GET request for the given user.
|
||||
func newReqWithUser(user model.User, endpoint string, params ...string) *http.Request {
|
||||
u := "/rest/" + endpoint
|
||||
if len(params) > 0 {
|
||||
q := url.Values{}
|
||||
for i := 0; i < len(params)-1; i += 2 {
|
||||
q.Add(params[i], params[i+1])
|
||||
}
|
||||
u += "?" + q.Encode()
|
||||
}
|
||||
r := httptest.NewRequest("GET", u, nil)
|
||||
userCtx := request.WithUser(r.Context(), user)
|
||||
userCtx = request.WithUsername(userCtx, user.UserName)
|
||||
userCtx = request.WithClient(userCtx, "test-client")
|
||||
userCtx = request.WithPlayer(userCtx, model.Player{ID: "player-1", Name: "Test Player", Client: "test-client"})
|
||||
return r.WithContext(userCtx)
|
||||
}
|
||||
|
||||
// newRawReq creates a ResponseRecorder + authenticated request for raw handlers (stream, download, getCoverArt).
|
||||
func newRawReq(endpoint string, params ...string) (*httptest.ResponseRecorder, *http.Request) {
|
||||
return httptest.NewRecorder(), newReq(endpoint, params...)
|
||||
}
|
||||
|
||||
// newRawReqWithUser creates a ResponseRecorder + authenticated request for the given user.
|
||||
func newRawReqWithUser(user model.User, endpoint string, params ...string) (*httptest.ResponseRecorder, *http.Request) {
|
||||
return httptest.NewRecorder(), newReqWithUser(user, endpoint, params...)
|
||||
}
|
||||
|
||||
// --- Noop stub implementations for Router dependencies ---
|
||||
|
||||
// noopArtwork implements artwork.Artwork
|
||||
type noopArtwork struct{}
|
||||
|
||||
func (n noopArtwork) Get(context.Context, model.ArtworkID, int, bool) (io.ReadCloser, time.Time, error) {
|
||||
return nil, time.Time{}, model.ErrNotFound
|
||||
}
|
||||
|
||||
func (n noopArtwork) GetOrPlaceholder(_ context.Context, _ string, _ int, _ bool) (io.ReadCloser, time.Time, error) {
|
||||
return io.NopCloser(io.LimitReader(nil, 0)), time.Time{}, nil
|
||||
}
|
||||
|
||||
// noopStreamer implements core.MediaStreamer
|
||||
type noopStreamer struct{}
|
||||
|
||||
func (n noopStreamer) NewStream(context.Context, string, string, int, int) (*core.Stream, error) {
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
|
||||
func (n noopStreamer) DoStream(context.Context, *model.MediaFile, string, int, int) (*core.Stream, error) {
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
|
||||
// noopArchiver implements core.Archiver
|
||||
type noopArchiver struct{}
|
||||
|
||||
func (n noopArchiver) ZipAlbum(context.Context, string, string, int, io.Writer) error {
|
||||
return model.ErrNotFound
|
||||
}
|
||||
|
||||
func (n noopArchiver) ZipArtist(context.Context, string, string, int, io.Writer) error {
|
||||
return model.ErrNotFound
|
||||
}
|
||||
|
||||
func (n noopArchiver) ZipShare(context.Context, string, io.Writer) error {
|
||||
return model.ErrNotFound
|
||||
}
|
||||
|
||||
func (n noopArchiver) ZipPlaylist(context.Context, string, string, int, io.Writer) error {
|
||||
return model.ErrNotFound
|
||||
}
|
||||
|
||||
// noopProvider implements external.Provider
|
||||
type noopProvider struct{}
|
||||
|
||||
func (n noopProvider) UpdateAlbumInfo(_ context.Context, _ string) (*model.Album, error) {
|
||||
return &model.Album{}, nil
|
||||
}
|
||||
|
||||
func (n noopProvider) UpdateArtistInfo(_ context.Context, _ string, _ int, _ bool) (*model.Artist, error) {
|
||||
return &model.Artist{}, nil
|
||||
}
|
||||
|
||||
func (n noopProvider) SimilarSongs(context.Context, string, int) (model.MediaFiles, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (n noopProvider) TopSongs(context.Context, string, int) (model.MediaFiles, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (n noopProvider) ArtistImage(context.Context, string) (*url.URL, error) {
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
|
||||
func (n noopProvider) AlbumImage(context.Context, string) (*url.URL, error) {
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
|
||||
// noopPlayTracker implements scrobbler.PlayTracker
|
||||
type noopPlayTracker struct{}
|
||||
|
||||
func (n noopPlayTracker) NowPlaying(context.Context, string, string, string, int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n noopPlayTracker) GetNowPlaying(context.Context) ([]scrobbler.NowPlayingInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (n noopPlayTracker) Submit(context.Context, []scrobbler.Submission) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Compile-time interface checks
|
||||
var (
|
||||
_ artwork.Artwork = noopArtwork{}
|
||||
_ core.MediaStreamer = noopStreamer{}
|
||||
_ core.Archiver = noopArchiver{}
|
||||
_ external.Provider = noopProvider{}
|
||||
_ scrobbler.PlayTracker = noopPlayTracker{}
|
||||
)
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
ctx = request.WithUser(GinkgoT().Context(), adminUser)
|
||||
tmpDir := GinkgoT().TempDir()
|
||||
dbFilePath = filepath.Join(tmpDir, "test-e2e.db")
|
||||
snapshotPath = filepath.Join(tmpDir, "test-e2e.db.snapshot")
|
||||
conf.Server.DbPath = dbFilePath + "?_journal_mode=WAL"
|
||||
db.Db().SetMaxOpenConns(1)
|
||||
|
||||
// Initial setup: schema, user, library, and full scan (runs once for the entire suite)
|
||||
conf.Server.MusicFolder = "fake:///music"
|
||||
conf.Server.DevExternalScanner = false
|
||||
|
||||
db.Init(ctx)
|
||||
|
||||
initDS := &tests.MockDataStore{RealDS: persistence.New(db.Db())}
|
||||
auth.Init(initDS)
|
||||
|
||||
adminUserWithPass := adminUser
|
||||
adminUserWithPass.NewPassword = "password"
|
||||
Expect(initDS.User(ctx).Put(&adminUserWithPass)).To(Succeed())
|
||||
|
||||
lib = model.Library{ID: 1, Name: "Music Library", Path: "fake:///music"}
|
||||
Expect(initDS.Library(ctx).Put(&lib)).To(Succeed())
|
||||
|
||||
Expect(initDS.User(ctx).SetUserLibraries(adminUser.ID, []int{lib.ID})).To(Succeed())
|
||||
|
||||
loadedUser, err := initDS.User(ctx).FindByUsername(adminUser.UserName)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
adminUser.Libraries = loadedUser.Libraries
|
||||
ctx = request.WithUser(GinkgoT().Context(), adminUser)
|
||||
|
||||
buildTestFS()
|
||||
s := scanner.New(ctx, initDS, artwork.NoopCacheWarmer(), events.NoopBroker(),
|
||||
core.NewPlaylists(initDS), metrics.NewNoopInstance())
|
||||
_, err = s.ScanAll(ctx, true)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Checkpoint WAL and snapshot the golden DB state
|
||||
_, err = db.Db().Exec("PRAGMA wal_checkpoint(TRUNCATE)")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
data, err := os.ReadFile(dbFilePath)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(os.WriteFile(snapshotPath, data, 0600)).To(Succeed())
|
||||
})
|
||||
|
||||
// setupTestDB restores the database from the golden snapshot and creates the
|
||||
// Subsonic Router. Call this from BeforeEach/BeforeAll in each test container.
|
||||
func setupTestDB() {
|
||||
ctx = request.WithUser(GinkgoT().Context(), adminUser)
|
||||
|
||||
DeferCleanup(configtest.SetupConfig())
|
||||
conf.Server.MusicFolder = "fake:///music"
|
||||
conf.Server.DevExternalScanner = false
|
||||
|
||||
// Restore DB to golden state (no scan needed)
|
||||
restoreDB()
|
||||
|
||||
ds = &tests.MockDataStore{RealDS: persistence.New(db.Db())}
|
||||
auth.Init(ds)
|
||||
|
||||
// Pre-populate repository cache with a valid context. The MockDataStore caches
|
||||
// repositories on first access; without this, the first access may happen inside
|
||||
// an errgroup (e.g., searchAll) whose context is canceled after Wait(), causing
|
||||
// subsequent calls to silently fail.
|
||||
ds.MediaFile(ctx)
|
||||
ds.Album(ctx)
|
||||
ds.Artist(ctx)
|
||||
|
||||
// Create the Subsonic Router with real DS + noop stubs
|
||||
s := scanner.New(ctx, ds, artwork.NoopCacheWarmer(), events.NoopBroker(),
|
||||
core.NewPlaylists(ds), metrics.NewNoopInstance())
|
||||
router = subsonic.New(
|
||||
ds,
|
||||
noopArtwork{},
|
||||
noopStreamer{},
|
||||
noopArchiver{},
|
||||
core.NewPlayers(ds),
|
||||
noopProvider{},
|
||||
s,
|
||||
events.NoopBroker(),
|
||||
core.NewPlaylists(ds),
|
||||
noopPlayTracker{},
|
||||
core.NewShare(ds),
|
||||
playback.PlaybackServer(nil),
|
||||
metrics.NewNoopInstance(),
|
||||
)
|
||||
}
|
||||
|
||||
// restoreDB restores all table data from the snapshot using ATTACH DATABASE.
|
||||
// This is much faster than re-running the scanner for each test.
|
||||
func restoreDB() {
|
||||
sqlDB := db.Db()
|
||||
|
||||
_, err := sqlDB.Exec("PRAGMA foreign_keys = OFF")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
_, err = sqlDB.Exec("ATTACH DATABASE ? AS snapshot", snapshotPath)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
rows, err := sqlDB.Query("SELECT name FROM main.sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
var tables []string
|
||||
for rows.Next() {
|
||||
var name string
|
||||
Expect(rows.Scan(&name)).To(Succeed())
|
||||
tables = append(tables, name)
|
||||
}
|
||||
Expect(rows.Err()).ToNot(HaveOccurred())
|
||||
rows.Close()
|
||||
|
||||
for _, table := range tables {
|
||||
// Table names come from sqlite_master, not user input, so concatenation is safe here
|
||||
_, err = sqlDB.Exec(`DELETE FROM main."` + table + `"`) //nolint:gosec
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
_, err = sqlDB.Exec(`INSERT INTO main."` + table + `" SELECT * FROM snapshot."` + table + `"`) //nolint:gosec
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
|
||||
_, err = sqlDB.Exec("DETACH DATABASE snapshot")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
_, err = sqlDB.Exec("PRAGMA foreign_keys = ON")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
350
server/e2e/subsonic_album_lists_test.go
Normal file
350
server/e2e/subsonic_album_lists_test.go
Normal file
@@ -0,0 +1,350 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Album List Endpoints", func() {
|
||||
BeforeEach(func() {
|
||||
setupTestDB()
|
||||
})
|
||||
|
||||
Describe("GetAlbumList", func() {
|
||||
It("type=newest returns albums sorted by creation date", func() {
|
||||
w, r := newRawReq("getAlbumList", "type", "newest")
|
||||
resp, err := router.GetAlbumList(w, r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.AlbumList).ToNot(BeNil())
|
||||
Expect(resp.AlbumList.Album).To(HaveLen(5))
|
||||
})
|
||||
|
||||
It("type=alphabeticalByName sorts albums by name", func() {
|
||||
w, r := newRawReq("getAlbumList", "type", "alphabeticalByName")
|
||||
resp, err := router.GetAlbumList(w, r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.AlbumList).ToNot(BeNil())
|
||||
albums := resp.AlbumList.Album
|
||||
Expect(albums).To(HaveLen(5))
|
||||
// Verify alphabetical order: Abbey Road, Help!, IV, Kind of Blue, Pop
|
||||
Expect(albums[0].Title).To(Equal("Abbey Road"))
|
||||
Expect(albums[1].Title).To(Equal("Help!"))
|
||||
Expect(albums[2].Title).To(Equal("IV"))
|
||||
Expect(albums[3].Title).To(Equal("Kind of Blue"))
|
||||
Expect(albums[4].Title).To(Equal("Pop"))
|
||||
})
|
||||
|
||||
It("type=alphabeticalByArtist sorts albums by artist name", func() {
|
||||
w, r := newRawReq("getAlbumList", "type", "alphabeticalByArtist")
|
||||
resp, err := router.GetAlbumList(w, r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.AlbumList).ToNot(BeNil())
|
||||
albums := resp.AlbumList.Album
|
||||
Expect(albums).To(HaveLen(5))
|
||||
// Articles like "The" are stripped for sorting, so "The Beatles" sorts as "Beatles"
|
||||
// Non-compilations first: Beatles (x2), Led Zeppelin, Miles Davis, then compilations: Various
|
||||
Expect(albums[0].Artist).To(Equal("The Beatles"))
|
||||
Expect(albums[2].Artist).To(Equal("Led Zeppelin"))
|
||||
Expect(albums[3].Artist).To(Equal("Miles Davis"))
|
||||
})
|
||||
|
||||
It("type=random returns albums", func() {
|
||||
w, r := newRawReq("getAlbumList", "type", "random")
|
||||
resp, err := router.GetAlbumList(w, r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.AlbumList).ToNot(BeNil())
|
||||
Expect(resp.AlbumList.Album).To(HaveLen(5))
|
||||
})
|
||||
|
||||
It("type=byGenre filters by genre parameter", func() {
|
||||
w, r := newRawReq("getAlbumList", "type", "byGenre", "genre", "Jazz")
|
||||
resp, err := router.GetAlbumList(w, r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.AlbumList).ToNot(BeNil())
|
||||
Expect(resp.AlbumList.Album).To(HaveLen(1))
|
||||
Expect(resp.AlbumList.Album[0].Title).To(Equal("Kind of Blue"))
|
||||
})
|
||||
|
||||
It("type=byYear filters by fromYear/toYear range", func() {
|
||||
w, r := newRawReq("getAlbumList", "type", "byYear", "fromYear", "1965", "toYear", "1970")
|
||||
resp, err := router.GetAlbumList(w, r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.AlbumList).ToNot(BeNil())
|
||||
// Should include Abbey Road (1969) and Help! (1965)
|
||||
Expect(resp.AlbumList.Album).To(HaveLen(2))
|
||||
years := make([]int32, len(resp.AlbumList.Album))
|
||||
for i, a := range resp.AlbumList.Album {
|
||||
years[i] = a.Year
|
||||
}
|
||||
Expect(years).To(ConsistOf(int32(1965), int32(1969)))
|
||||
})
|
||||
|
||||
It("respects size parameter", func() {
|
||||
w, r := newRawReq("getAlbumList", "type", "newest", "size", "2")
|
||||
resp, err := router.GetAlbumList(w, r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.AlbumList).ToNot(BeNil())
|
||||
Expect(resp.AlbumList.Album).To(HaveLen(2))
|
||||
})
|
||||
|
||||
It("supports offset for pagination", func() {
|
||||
// First get all albums sorted by name to know the expected order
|
||||
w1, r1 := newRawReq("getAlbumList", "type", "alphabeticalByName", "size", "5")
|
||||
resp1, err := router.GetAlbumList(w1, r1)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
allAlbums := resp1.AlbumList.Album
|
||||
|
||||
// Now get with offset=2, size=2
|
||||
w2, r2 := newRawReq("getAlbumList", "type", "alphabeticalByName", "size", "2", "offset", "2")
|
||||
resp2, err := router.GetAlbumList(w2, r2)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp2.AlbumList).ToNot(BeNil())
|
||||
Expect(resp2.AlbumList.Album).To(HaveLen(2))
|
||||
Expect(resp2.AlbumList.Album[0].Title).To(Equal(allAlbums[2].Title))
|
||||
Expect(resp2.AlbumList.Album[1].Title).To(Equal(allAlbums[3].Title))
|
||||
})
|
||||
|
||||
It("returns error when type parameter is missing", func() {
|
||||
w := httptest.NewRecorder()
|
||||
r := newReq("getAlbumList")
|
||||
_, err := router.GetAlbumList(w, r)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err).To(MatchError(req.ErrMissingParam))
|
||||
})
|
||||
|
||||
It("returns error for unknown type", func() {
|
||||
w, r := newRawReq("getAlbumList", "type", "invalid_type")
|
||||
_, err := router.GetAlbumList(w, r)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("type=frequent returns empty when no albums have been played", func() {
|
||||
w, r := newRawReq("getAlbumList", "type", "frequent")
|
||||
resp, err := router.GetAlbumList(w, r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.AlbumList).ToNot(BeNil())
|
||||
Expect(resp.AlbumList.Album).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("type=recent returns empty when no albums have been played", func() {
|
||||
w, r := newRawReq("getAlbumList", "type", "recent")
|
||||
resp, err := router.GetAlbumList(w, r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.AlbumList).ToNot(BeNil())
|
||||
Expect(resp.AlbumList.Album).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetAlbumList - starred type", Ordered, func() {
|
||||
BeforeAll(func() {
|
||||
setupTestDB()
|
||||
|
||||
// Star an album so the starred filter returns results
|
||||
albums, err := ds.Album(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"album.name": "Abbey Road"},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(albums).ToNot(BeEmpty())
|
||||
|
||||
r := newReq("star", "albumId", albums[0].ID)
|
||||
_, err = router.Star(r)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("type=starred returns only starred albums", func() {
|
||||
w, r := newRawReq("getAlbumList", "type", "starred")
|
||||
resp, err := router.GetAlbumList(w, r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.AlbumList).ToNot(BeNil())
|
||||
Expect(resp.AlbumList.Album).To(HaveLen(1))
|
||||
Expect(resp.AlbumList.Album[0].Title).To(Equal("Abbey Road"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetAlbumList - highest type", Ordered, func() {
|
||||
BeforeAll(func() {
|
||||
setupTestDB()
|
||||
|
||||
// Rate an album so the highest filter returns results
|
||||
albums, err := ds.Album(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"album.name": "Kind of Blue"},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(albums).ToNot(BeEmpty())
|
||||
|
||||
r := newReq("setRating", "id", albums[0].ID, "rating", "5")
|
||||
_, err = router.SetRating(r)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("type=highest returns only rated albums", func() {
|
||||
w, r := newRawReq("getAlbumList", "type", "highest")
|
||||
resp, err := router.GetAlbumList(w, r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.AlbumList).ToNot(BeNil())
|
||||
Expect(resp.AlbumList.Album).To(HaveLen(1))
|
||||
Expect(resp.AlbumList.Album[0].Title).To(Equal("Kind of Blue"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetAlbumList2", func() {
|
||||
It("returns albums in AlbumID3 format", func() {
|
||||
w, r := newRawReq("getAlbumList2", "type", "alphabeticalByName")
|
||||
resp, err := router.GetAlbumList2(w, r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.AlbumList2).ToNot(BeNil())
|
||||
albums := resp.AlbumList2.Album
|
||||
Expect(albums).To(HaveLen(5))
|
||||
// Verify AlbumID3 format fields
|
||||
Expect(albums[0].Name).To(Equal("Abbey Road"))
|
||||
Expect(albums[0].Id).ToNot(BeEmpty())
|
||||
Expect(albums[0].Artist).ToNot(BeEmpty())
|
||||
})
|
||||
|
||||
It("type=newest works correctly", func() {
|
||||
w, r := newRawReq("getAlbumList2", "type", "newest")
|
||||
resp, err := router.GetAlbumList2(w, r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.AlbumList2).ToNot(BeNil())
|
||||
Expect(resp.AlbumList2.Album).To(HaveLen(5))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetStarred", func() {
|
||||
It("returns empty lists when nothing is starred", func() {
|
||||
r := newReq("getStarred")
|
||||
resp, err := router.GetStarred(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.Starred).ToNot(BeNil())
|
||||
Expect(resp.Starred.Artist).To(BeEmpty())
|
||||
Expect(resp.Starred.Album).To(BeEmpty())
|
||||
Expect(resp.Starred.Song).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetStarred2", func() {
|
||||
It("returns empty lists when nothing is starred", func() {
|
||||
r := newReq("getStarred2")
|
||||
resp, err := router.GetStarred2(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.Starred2).ToNot(BeNil())
|
||||
Expect(resp.Starred2.Artist).To(BeEmpty())
|
||||
Expect(resp.Starred2.Album).To(BeEmpty())
|
||||
Expect(resp.Starred2.Song).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetNowPlaying", func() {
|
||||
It("returns empty list when nobody is playing", func() {
|
||||
r := newReq("getNowPlaying")
|
||||
resp, err := router.GetNowPlaying(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.NowPlaying).ToNot(BeNil())
|
||||
Expect(resp.NowPlaying.Entry).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetRandomSongs", func() {
|
||||
It("returns random songs from library", func() {
|
||||
r := newReq("getRandomSongs")
|
||||
resp, err := router.GetRandomSongs(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.RandomSongs).ToNot(BeNil())
|
||||
Expect(resp.RandomSongs.Songs).ToNot(BeEmpty())
|
||||
Expect(len(resp.RandomSongs.Songs)).To(BeNumerically("<=", 6))
|
||||
})
|
||||
|
||||
It("respects size parameter", func() {
|
||||
r := newReq("getRandomSongs", "size", "2")
|
||||
resp, err := router.GetRandomSongs(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.RandomSongs).ToNot(BeNil())
|
||||
Expect(resp.RandomSongs.Songs).To(HaveLen(2))
|
||||
})
|
||||
|
||||
It("filters by genre when specified", func() {
|
||||
r := newReq("getRandomSongs", "size", "500", "genre", "Jazz")
|
||||
resp, err := router.GetRandomSongs(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.RandomSongs).ToNot(BeNil())
|
||||
Expect(resp.RandomSongs.Songs).To(HaveLen(1))
|
||||
Expect(resp.RandomSongs.Songs[0].Genre).To(Equal("Jazz"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetSongsByGenre", func() {
|
||||
It("returns songs matching the genre", func() {
|
||||
r := newReq("getSongsByGenre", "genre", "Rock")
|
||||
resp, err := router.GetSongsByGenre(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.SongsByGenre).ToNot(BeNil())
|
||||
// 4 Rock songs: Come Together, Something, Help!, Stairway To Heaven
|
||||
Expect(resp.SongsByGenre.Songs).To(HaveLen(4))
|
||||
for _, song := range resp.SongsByGenre.Songs {
|
||||
Expect(song.Genre).To(Equal("Rock"))
|
||||
}
|
||||
})
|
||||
|
||||
It("supports count and offset parameters", func() {
|
||||
// First get all Rock songs
|
||||
r1 := newReq("getSongsByGenre", "genre", "Rock", "count", "500")
|
||||
resp1, err := router.GetSongsByGenre(r1)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
allSongs := resp1.SongsByGenre.Songs
|
||||
|
||||
// Now get with count=2, offset=1
|
||||
r2 := newReq("getSongsByGenre", "genre", "Rock", "count", "2", "offset", "1")
|
||||
resp2, err := router.GetSongsByGenre(r2)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp2.SongsByGenre).ToNot(BeNil())
|
||||
Expect(resp2.SongsByGenre.Songs).To(HaveLen(2))
|
||||
Expect(resp2.SongsByGenre.Songs[0].Id).To(Equal(allSongs[1].Id))
|
||||
})
|
||||
|
||||
It("returns empty for non-existent genre", func() {
|
||||
r := newReq("getSongsByGenre", "genre", "NonExistentGenre")
|
||||
resp, err := router.GetSongsByGenre(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.SongsByGenre).ToNot(BeNil())
|
||||
Expect(resp.SongsByGenre.Songs).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
})
|
||||
164
server/e2e/subsonic_bookmarks_test.go
Normal file
164
server/e2e/subsonic_bookmarks_test.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Bookmark and PlayQueue Endpoints", Ordered, func() {
|
||||
BeforeAll(func() {
|
||||
setupTestDB()
|
||||
})
|
||||
|
||||
Describe("Bookmark Endpoints", Ordered, func() {
|
||||
var trackID string
|
||||
|
||||
BeforeAll(func() {
|
||||
// Get a media file ID from the database to use for bookmarks
|
||||
mfs, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{Max: 1})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mfs).ToNot(BeEmpty())
|
||||
trackID = mfs[0].ID
|
||||
})
|
||||
|
||||
It("getBookmarks returns empty initially", func() {
|
||||
r := newReq("getBookmarks")
|
||||
resp, err := router.GetBookmarks(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.Bookmarks).ToNot(BeNil())
|
||||
Expect(resp.Bookmarks.Bookmark).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("createBookmark creates a bookmark with position", func() {
|
||||
r := newReq("createBookmark", "id", trackID, "position", "12345", "comment", "test bookmark")
|
||||
resp, err := router.CreateBookmark(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
})
|
||||
|
||||
It("getBookmarks shows the created bookmark", func() {
|
||||
r := newReq("getBookmarks")
|
||||
resp, err := router.GetBookmarks(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.Bookmarks).ToNot(BeNil())
|
||||
Expect(resp.Bookmarks.Bookmark).To(HaveLen(1))
|
||||
|
||||
bmk := resp.Bookmarks.Bookmark[0]
|
||||
Expect(bmk.Entry.Id).To(Equal(trackID))
|
||||
Expect(bmk.Position).To(Equal(int64(12345)))
|
||||
Expect(bmk.Comment).To(Equal("test bookmark"))
|
||||
Expect(bmk.Username).To(Equal(adminUser.UserName))
|
||||
})
|
||||
|
||||
It("deleteBookmark removes the bookmark", func() {
|
||||
r := newReq("deleteBookmark", "id", trackID)
|
||||
resp, err := router.DeleteBookmark(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
|
||||
// Verify it's gone
|
||||
r = newReq("getBookmarks")
|
||||
resp, err = router.GetBookmarks(r)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Bookmarks.Bookmark).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("PlayQueue Endpoints", Ordered, func() {
|
||||
var trackIDs []string
|
||||
|
||||
BeforeAll(func() {
|
||||
// Get multiple media file IDs from the database
|
||||
mfs, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{Max: 3, Sort: "title"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(len(mfs)).To(BeNumerically(">=", 2))
|
||||
for _, mf := range mfs {
|
||||
trackIDs = append(trackIDs, mf.ID)
|
||||
}
|
||||
})
|
||||
|
||||
It("getPlayQueue returns empty when nothing saved", func() {
|
||||
r := newReq("getPlayQueue")
|
||||
resp, err := router.GetPlayQueue(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
// When no play queue exists, PlayQueue should be nil (no entry returned)
|
||||
Expect(resp.PlayQueue).To(BeNil())
|
||||
})
|
||||
|
||||
It("savePlayQueue stores current play queue", func() {
|
||||
r := newReq("savePlayQueue",
|
||||
"id", trackIDs[0],
|
||||
"id", trackIDs[1],
|
||||
"current", trackIDs[1],
|
||||
"position", "5000",
|
||||
)
|
||||
resp, err := router.SavePlayQueue(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
})
|
||||
|
||||
It("getPlayQueue returns saved queue with tracks", func() {
|
||||
r := newReq("getPlayQueue")
|
||||
resp, err := router.GetPlayQueue(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.PlayQueue).ToNot(BeNil())
|
||||
Expect(resp.PlayQueue.Entry).To(HaveLen(2))
|
||||
Expect(resp.PlayQueue.Current).To(Equal(trackIDs[1]))
|
||||
Expect(resp.PlayQueue.Position).To(Equal(int64(5000)))
|
||||
Expect(resp.PlayQueue.Username).To(Equal(adminUser.UserName))
|
||||
Expect(resp.PlayQueue.ChangedBy).To(Equal("test-client"))
|
||||
})
|
||||
|
||||
It("getPlayQueueByIndex returns data with current index", func() {
|
||||
r := newReq("getPlayQueueByIndex")
|
||||
resp, err := router.GetPlayQueueByIndex(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.PlayQueueByIndex).ToNot(BeNil())
|
||||
Expect(resp.PlayQueueByIndex.Entry).To(HaveLen(2))
|
||||
Expect(resp.PlayQueueByIndex.CurrentIndex).ToNot(BeNil())
|
||||
Expect(*resp.PlayQueueByIndex.CurrentIndex).To(Equal(1))
|
||||
Expect(resp.PlayQueueByIndex.Position).To(Equal(int64(5000)))
|
||||
})
|
||||
|
||||
It("savePlayQueueByIndex stores queue by index", func() {
|
||||
r := newReq("savePlayQueueByIndex",
|
||||
"id", trackIDs[0],
|
||||
"id", trackIDs[1],
|
||||
"id", trackIDs[2],
|
||||
"currentIndex", fmt.Sprintf("%d", 0),
|
||||
"position", "9999",
|
||||
)
|
||||
resp, err := router.SavePlayQueueByIndex(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
|
||||
// Verify with getPlayQueueByIndex
|
||||
r = newReq("getPlayQueueByIndex")
|
||||
resp, err = router.GetPlayQueueByIndex(r)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.PlayQueueByIndex).ToNot(BeNil())
|
||||
Expect(resp.PlayQueueByIndex.Entry).To(HaveLen(3))
|
||||
Expect(resp.PlayQueueByIndex.CurrentIndex).ToNot(BeNil())
|
||||
Expect(*resp.PlayQueueByIndex.CurrentIndex).To(Equal(0))
|
||||
Expect(resp.PlayQueueByIndex.Position).To(Equal(int64(9999)))
|
||||
})
|
||||
})
|
||||
})
|
||||
522
server/e2e/subsonic_browsing_test.go
Normal file
522
server/e2e/subsonic_browsing_test.go
Normal file
@@ -0,0 +1,522 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Browsing Endpoints", func() {
|
||||
BeforeEach(func() {
|
||||
setupTestDB()
|
||||
})
|
||||
|
||||
Describe("getMusicFolders", func() {
|
||||
It("returns the configured music library", func() {
|
||||
r := newReq("getMusicFolders")
|
||||
resp, err := router.GetMusicFolders(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.MusicFolders).ToNot(BeNil())
|
||||
Expect(resp.MusicFolders.Folders).To(HaveLen(1))
|
||||
Expect(resp.MusicFolders.Folders[0].Name).To(Equal("Music Library"))
|
||||
Expect(resp.MusicFolders.Folders[0].Id).To(Equal(int32(lib.ID)))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("getIndexes", func() {
|
||||
It("returns artist indexes", func() {
|
||||
r := newReq("getIndexes")
|
||||
resp, err := router.GetIndexes(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.Indexes).ToNot(BeNil())
|
||||
Expect(resp.Indexes.Index).ToNot(BeEmpty())
|
||||
})
|
||||
|
||||
It("includes all artists across indexes", func() {
|
||||
r := newReq("getIndexes")
|
||||
resp, err := router.GetIndexes(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
var allArtistNames []string
|
||||
for _, idx := range resp.Indexes.Index {
|
||||
for _, a := range idx.Artists {
|
||||
allArtistNames = append(allArtistNames, a.Name)
|
||||
}
|
||||
}
|
||||
Expect(allArtistNames).To(ContainElements("The Beatles", "Led Zeppelin", "Miles Davis", "Various"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("getArtists", func() {
|
||||
It("returns artist indexes in ID3 format", func() {
|
||||
r := newReq("getArtists")
|
||||
resp, err := router.GetArtists(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.Artist).ToNot(BeNil())
|
||||
Expect(resp.Artist.Index).ToNot(BeEmpty())
|
||||
})
|
||||
|
||||
It("includes all artists across ID3 indexes", func() {
|
||||
r := newReq("getArtists")
|
||||
resp, err := router.GetArtists(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
var allArtistNames []string
|
||||
for _, idx := range resp.Artist.Index {
|
||||
for _, a := range idx.Artists {
|
||||
allArtistNames = append(allArtistNames, a.Name)
|
||||
}
|
||||
}
|
||||
Expect(allArtistNames).To(ContainElements("The Beatles", "Led Zeppelin", "Miles Davis", "Various"))
|
||||
})
|
||||
|
||||
It("reports correct album counts for artists", func() {
|
||||
r := newReq("getArtists")
|
||||
resp, err := router.GetArtists(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
var beatlesAlbumCount int32
|
||||
for _, idx := range resp.Artist.Index {
|
||||
for _, a := range idx.Artists {
|
||||
if a.Name == "The Beatles" {
|
||||
beatlesAlbumCount = a.AlbumCount
|
||||
}
|
||||
}
|
||||
}
|
||||
Expect(beatlesAlbumCount).To(Equal(int32(2)))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("getMusicDirectory", func() {
|
||||
It("returns an artist directory with its albums as children", func() {
|
||||
artists, err := ds.Artist(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"name": "The Beatles"},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(artists).ToNot(BeEmpty())
|
||||
beatlesID := artists[0].ID
|
||||
|
||||
r := newReq("getMusicDirectory", "id", beatlesID)
|
||||
resp, err := router.GetMusicDirectory(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.Directory).ToNot(BeNil())
|
||||
Expect(resp.Directory.Name).To(Equal("The Beatles"))
|
||||
Expect(resp.Directory.Child).To(HaveLen(2)) // Abbey Road, Help!
|
||||
})
|
||||
|
||||
It("returns an album directory with its tracks as children", func() {
|
||||
albums, err := ds.Album(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"album.name": "Abbey Road"},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(albums).ToNot(BeEmpty())
|
||||
abbeyRoadID := albums[0].ID
|
||||
|
||||
r := newReq("getMusicDirectory", "id", abbeyRoadID)
|
||||
resp, err := router.GetMusicDirectory(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Directory).ToNot(BeNil())
|
||||
Expect(resp.Directory.Name).To(Equal("Abbey Road"))
|
||||
Expect(resp.Directory.Child).To(HaveLen(2)) // Come Together, Something
|
||||
})
|
||||
|
||||
It("returns an error for a non-existent ID", func() {
|
||||
r := newReq("getMusicDirectory", "id", "non-existent-id")
|
||||
_, err := router.GetMusicDirectory(r)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("getArtist", func() {
|
||||
It("returns artist with albums in ID3 format", func() {
|
||||
artists, err := ds.Artist(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"name": "The Beatles"},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(artists).ToNot(BeEmpty())
|
||||
beatlesID := artists[0].ID
|
||||
|
||||
r := newReq("getArtist", "id", beatlesID)
|
||||
resp, err := router.GetArtist(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.ArtistWithAlbumsID3).ToNot(BeNil())
|
||||
Expect(resp.ArtistWithAlbumsID3.Name).To(Equal("The Beatles"))
|
||||
Expect(resp.ArtistWithAlbumsID3.Album).To(HaveLen(2))
|
||||
})
|
||||
|
||||
It("returns album names for the artist", func() {
|
||||
artists, err := ds.Artist(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"name": "The Beatles"},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(artists).ToNot(BeEmpty())
|
||||
beatlesID := artists[0].ID
|
||||
|
||||
r := newReq("getArtist", "id", beatlesID)
|
||||
resp, err := router.GetArtist(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
var albumNames []string
|
||||
for _, a := range resp.ArtistWithAlbumsID3.Album {
|
||||
albumNames = append(albumNames, a.Name)
|
||||
}
|
||||
Expect(albumNames).To(ContainElements("Abbey Road", "Help!"))
|
||||
})
|
||||
|
||||
It("returns an error for a non-existent artist", func() {
|
||||
r := newReq("getArtist", "id", "non-existent-id")
|
||||
_, err := router.GetArtist(r)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns artist with a single album", func() {
|
||||
artists, err := ds.Artist(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"name": "Led Zeppelin"},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(artists).ToNot(BeEmpty())
|
||||
ledZepID := artists[0].ID
|
||||
|
||||
r := newReq("getArtist", "id", ledZepID)
|
||||
resp, err := router.GetArtist(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.ArtistWithAlbumsID3).ToNot(BeNil())
|
||||
Expect(resp.ArtistWithAlbumsID3.Name).To(Equal("Led Zeppelin"))
|
||||
Expect(resp.ArtistWithAlbumsID3.Album).To(HaveLen(1))
|
||||
Expect(resp.ArtistWithAlbumsID3.Album[0].Name).To(Equal("IV"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("getAlbum", func() {
|
||||
It("returns album with its tracks", func() {
|
||||
albums, err := ds.Album(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"album.name": "Abbey Road"},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(albums).ToNot(BeEmpty())
|
||||
abbeyRoadID := albums[0].ID
|
||||
|
||||
r := newReq("getAlbum", "id", abbeyRoadID)
|
||||
resp, err := router.GetAlbum(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.AlbumWithSongsID3).ToNot(BeNil())
|
||||
Expect(resp.AlbumWithSongsID3.Name).To(Equal("Abbey Road"))
|
||||
Expect(resp.AlbumWithSongsID3.Song).To(HaveLen(2))
|
||||
})
|
||||
|
||||
It("includes correct track metadata", func() {
|
||||
albums, err := ds.Album(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"album.name": "Abbey Road"},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(albums).ToNot(BeEmpty())
|
||||
abbeyRoadID := albums[0].ID
|
||||
|
||||
r := newReq("getAlbum", "id", abbeyRoadID)
|
||||
resp, err := router.GetAlbum(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
var trackTitles []string
|
||||
for _, s := range resp.AlbumWithSongsID3.Song {
|
||||
trackTitles = append(trackTitles, s.Title)
|
||||
}
|
||||
Expect(trackTitles).To(ContainElements("Come Together", "Something"))
|
||||
})
|
||||
|
||||
It("returns album with correct artist and year", func() {
|
||||
albums, err := ds.Album(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"album.name": "Kind of Blue"},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(albums).ToNot(BeEmpty())
|
||||
kindOfBlueID := albums[0].ID
|
||||
|
||||
r := newReq("getAlbum", "id", kindOfBlueID)
|
||||
resp, err := router.GetAlbum(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.AlbumWithSongsID3).ToNot(BeNil())
|
||||
Expect(resp.AlbumWithSongsID3.Name).To(Equal("Kind of Blue"))
|
||||
Expect(resp.AlbumWithSongsID3.Artist).To(Equal("Miles Davis"))
|
||||
Expect(resp.AlbumWithSongsID3.Year).To(Equal(int32(1959)))
|
||||
Expect(resp.AlbumWithSongsID3.Song).To(HaveLen(1))
|
||||
})
|
||||
|
||||
It("returns an error for a non-existent album", func() {
|
||||
r := newReq("getAlbum", "id", "non-existent-id")
|
||||
_, err := router.GetAlbum(r)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("getSong", func() {
|
||||
It("returns a song by its ID", func() {
|
||||
songs, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"title": "Come Together"},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(songs).ToNot(BeEmpty())
|
||||
songID := songs[0].ID
|
||||
|
||||
r := newReq("getSong", "id", songID)
|
||||
resp, err := router.GetSong(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.Song).ToNot(BeNil())
|
||||
Expect(resp.Song.Title).To(Equal("Come Together"))
|
||||
Expect(resp.Song.Album).To(Equal("Abbey Road"))
|
||||
Expect(resp.Song.Artist).To(Equal("The Beatles"))
|
||||
})
|
||||
|
||||
It("returns an error for a non-existent song", func() {
|
||||
r := newReq("getSong", "id", "non-existent-id")
|
||||
_, err := router.GetSong(r)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns correct metadata for a jazz track", func() {
|
||||
songs, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"title": "So What"},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(songs).ToNot(BeEmpty())
|
||||
songID := songs[0].ID
|
||||
|
||||
r := newReq("getSong", "id", songID)
|
||||
resp, err := router.GetSong(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Song).ToNot(BeNil())
|
||||
Expect(resp.Song.Title).To(Equal("So What"))
|
||||
Expect(resp.Song.Album).To(Equal("Kind of Blue"))
|
||||
Expect(resp.Song.Artist).To(Equal("Miles Davis"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("getGenres", func() {
|
||||
It("returns all genres", func() {
|
||||
r := newReq("getGenres")
|
||||
resp, err := router.GetGenres(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.Genres).ToNot(BeNil())
|
||||
Expect(resp.Genres.Genre).To(HaveLen(3))
|
||||
})
|
||||
|
||||
It("includes correct genre names", func() {
|
||||
r := newReq("getGenres")
|
||||
resp, err := router.GetGenres(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
var genreNames []string
|
||||
for _, g := range resp.Genres.Genre {
|
||||
genreNames = append(genreNames, g.Name)
|
||||
}
|
||||
Expect(genreNames).To(ContainElements("Rock", "Jazz", "Pop"))
|
||||
})
|
||||
|
||||
It("reports correct song and album counts for Rock", func() {
|
||||
r := newReq("getGenres")
|
||||
resp, err := router.GetGenres(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
var rockGenre *responses.Genre
|
||||
for i, g := range resp.Genres.Genre {
|
||||
if g.Name == "Rock" {
|
||||
rockGenre = &resp.Genres.Genre[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
Expect(rockGenre).ToNot(BeNil())
|
||||
Expect(rockGenre.SongCount).To(Equal(int32(4)))
|
||||
Expect(rockGenre.AlbumCount).To(Equal(int32(3)))
|
||||
})
|
||||
|
||||
It("reports correct song and album counts for Jazz", func() {
|
||||
r := newReq("getGenres")
|
||||
resp, err := router.GetGenres(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
var jazzGenre *responses.Genre
|
||||
for i, g := range resp.Genres.Genre {
|
||||
if g.Name == "Jazz" {
|
||||
jazzGenre = &resp.Genres.Genre[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
Expect(jazzGenre).ToNot(BeNil())
|
||||
Expect(jazzGenre.SongCount).To(Equal(int32(1)))
|
||||
Expect(jazzGenre.AlbumCount).To(Equal(int32(1)))
|
||||
})
|
||||
|
||||
It("reports correct song and album counts for Pop", func() {
|
||||
r := newReq("getGenres")
|
||||
resp, err := router.GetGenres(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
var popGenre *responses.Genre
|
||||
for i, g := range resp.Genres.Genre {
|
||||
if g.Name == "Pop" {
|
||||
popGenre = &resp.Genres.Genre[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
Expect(popGenre).ToNot(BeNil())
|
||||
Expect(popGenre.SongCount).To(Equal(int32(1)))
|
||||
Expect(popGenre.AlbumCount).To(Equal(int32(1)))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("getAlbumInfo", func() {
|
||||
It("returns album info for a valid album", func() {
|
||||
albums, err := ds.Album(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"album.name": "Abbey Road"},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(albums).ToNot(BeEmpty())
|
||||
abbeyRoadID := albums[0].ID
|
||||
|
||||
r := newReq("getAlbumInfo", "id", abbeyRoadID)
|
||||
resp, err := router.GetAlbumInfo(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.AlbumInfo).ToNot(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("getAlbumInfo2", func() {
|
||||
It("returns album info for a valid album", func() {
|
||||
albums, err := ds.Album(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"album.name": "Abbey Road"},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(albums).ToNot(BeEmpty())
|
||||
abbeyRoadID := albums[0].ID
|
||||
|
||||
r := newReq("getAlbumInfo2", "id", abbeyRoadID)
|
||||
resp, err := router.GetAlbumInfo(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.AlbumInfo).ToNot(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("getArtistInfo", func() {
|
||||
It("returns artist info for a valid artist", func() {
|
||||
artists, err := ds.Artist(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"name": "The Beatles"},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(artists).ToNot(BeEmpty())
|
||||
beatlesID := artists[0].ID
|
||||
|
||||
r := newReq("getArtistInfo", "id", beatlesID)
|
||||
resp, err := router.GetArtistInfo(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.ArtistInfo).ToNot(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("getArtistInfo2", func() {
|
||||
It("returns artist info2 for a valid artist", func() {
|
||||
artists, err := ds.Artist(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"name": "The Beatles"},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(artists).ToNot(BeEmpty())
|
||||
beatlesID := artists[0].ID
|
||||
|
||||
r := newReq("getArtistInfo2", "id", beatlesID)
|
||||
resp, err := router.GetArtistInfo2(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.ArtistInfo2).ToNot(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("getTopSongs", func() {
|
||||
It("returns a response for a known artist name", func() {
|
||||
r := newReq("getTopSongs", "artist", "The Beatles")
|
||||
resp, err := router.GetTopSongs(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.TopSongs).ToNot(BeNil())
|
||||
// noopProvider returns empty list, so Songs may be empty
|
||||
})
|
||||
|
||||
It("returns an empty list for an unknown artist", func() {
|
||||
r := newReq("getTopSongs", "artist", "Unknown Artist")
|
||||
resp, err := router.GetTopSongs(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.TopSongs).ToNot(BeNil())
|
||||
Expect(resp.TopSongs.Song).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("getSimilarSongs", func() {
|
||||
It("returns a response for a valid song ID", func() {
|
||||
songs, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"title": "Come Together"},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(songs).ToNot(BeEmpty())
|
||||
songID := songs[0].ID
|
||||
|
||||
r := newReq("getSimilarSongs", "id", songID)
|
||||
resp, err := router.GetSimilarSongs(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.SimilarSongs).ToNot(BeNil())
|
||||
// noopProvider returns empty list
|
||||
})
|
||||
})
|
||||
|
||||
Describe("getSimilarSongs2", func() {
|
||||
It("returns a response for a valid song ID", func() {
|
||||
songs, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"title": "Come Together"},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(songs).ToNot(BeEmpty())
|
||||
songID := songs[0].ID
|
||||
|
||||
r := newReq("getSimilarSongs2", "id", songID)
|
||||
resp, err := router.GetSimilarSongs2(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.SimilarSongs2).ToNot(BeNil())
|
||||
// noopProvider returns empty list
|
||||
})
|
||||
})
|
||||
})
|
||||
186
server/e2e/subsonic_media_annotation_test.go
Normal file
186
server/e2e/subsonic_media_annotation_test.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Media Annotation Endpoints", Ordered, func() {
|
||||
BeforeAll(func() {
|
||||
setupTestDB()
|
||||
})
|
||||
|
||||
Describe("Star/Unstar", Ordered, func() {
|
||||
var songID, albumID, artistID string
|
||||
|
||||
BeforeAll(func() {
|
||||
// Look up a song from the scanned data
|
||||
songs, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{Max: 1, Sort: "title"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(songs).ToNot(BeEmpty())
|
||||
songID = songs[0].ID
|
||||
|
||||
// Look up an album
|
||||
albums, err := ds.Album(ctx).GetAll(model.QueryOptions{Max: 1, Sort: "name"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(albums).ToNot(BeEmpty())
|
||||
albumID = albums[0].ID
|
||||
|
||||
// Look up an artist
|
||||
artists, err := ds.Artist(ctx).GetAll(model.QueryOptions{Max: 1, Sort: "name"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(artists).ToNot(BeEmpty())
|
||||
artistID = artists[0].ID
|
||||
})
|
||||
|
||||
It("stars a song by id", func() {
|
||||
r := newReq("star", "id", songID)
|
||||
resp, err := router.Star(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
})
|
||||
|
||||
It("starred song appears in getStarred response", func() {
|
||||
r := newReq("getStarred")
|
||||
resp, err := router.GetStarred(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.Starred).ToNot(BeNil())
|
||||
Expect(resp.Starred.Song).To(HaveLen(1))
|
||||
Expect(resp.Starred.Song[0].Id).To(Equal(songID))
|
||||
})
|
||||
|
||||
It("unstars a previously starred song", func() {
|
||||
r := newReq("unstar", "id", songID)
|
||||
resp, err := router.Unstar(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
|
||||
// Verify song no longer appears in starred
|
||||
r = newReq("getStarred")
|
||||
resp, err = router.GetStarred(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Starred.Song).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("stars an album by albumId", func() {
|
||||
r := newReq("star", "albumId", albumID)
|
||||
resp, err := router.Star(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
|
||||
// Verify album appears in starred
|
||||
r = newReq("getStarred")
|
||||
resp, err = router.GetStarred(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Starred.Album).To(HaveLen(1))
|
||||
Expect(resp.Starred.Album[0].Id).To(Equal(albumID))
|
||||
})
|
||||
|
||||
It("stars an artist by artistId", func() {
|
||||
r := newReq("star", "artistId", artistID)
|
||||
resp, err := router.Star(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
|
||||
// Verify artist appears in starred
|
||||
r = newReq("getStarred")
|
||||
resp, err = router.GetStarred(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Starred.Artist).To(HaveLen(1))
|
||||
Expect(resp.Starred.Artist[0].Id).To(Equal(artistID))
|
||||
})
|
||||
|
||||
It("returns error when no id provided", func() {
|
||||
r := newReq("star")
|
||||
_, err := router.Star(r)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("SetRating", Ordered, func() {
|
||||
var songID, albumID string
|
||||
|
||||
BeforeAll(func() {
|
||||
songs, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{Max: 1, Sort: "title"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(songs).ToNot(BeEmpty())
|
||||
songID = songs[0].ID
|
||||
|
||||
albums, err := ds.Album(ctx).GetAll(model.QueryOptions{Max: 1, Sort: "name"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(albums).ToNot(BeEmpty())
|
||||
albumID = albums[0].ID
|
||||
})
|
||||
|
||||
It("sets rating on a song", func() {
|
||||
r := newReq("setRating", "id", songID, "rating", "4")
|
||||
resp, err := router.SetRating(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
})
|
||||
|
||||
It("rated song has correct userRating in getSong", func() {
|
||||
r := newReq("getSong", "id", songID)
|
||||
resp, err := router.GetSong(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.Song).ToNot(BeNil())
|
||||
Expect(resp.Song.UserRating).To(Equal(int32(4)))
|
||||
})
|
||||
|
||||
It("sets rating on an album", func() {
|
||||
r := newReq("setRating", "id", albumID, "rating", "3")
|
||||
resp, err := router.SetRating(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
})
|
||||
|
||||
It("returns error for missing parameters", func() {
|
||||
// Missing both id and rating
|
||||
r := newReq("setRating")
|
||||
_, err := router.SetRating(r)
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
// Missing rating
|
||||
r = newReq("setRating", "id", songID)
|
||||
_, err = router.SetRating(r)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Scrobble", func() {
|
||||
It("submits a scrobble for a song", func() {
|
||||
songs, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{Max: 1, Sort: "title"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(songs).ToNot(BeEmpty())
|
||||
|
||||
r := newReq("scrobble", "id", songs[0].ID, "submission", "true")
|
||||
resp, err := router.Scrobble(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
})
|
||||
|
||||
It("returns error when id is missing", func() {
|
||||
r := newReq("scrobble")
|
||||
_, err := router.Scrobble(r)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
})
|
||||
79
server/e2e/subsonic_media_retrieval_test.go
Normal file
79
server/e2e/subsonic_media_retrieval_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Media Retrieval Endpoints", Ordered, func() {
|
||||
BeforeAll(func() {
|
||||
setupTestDB()
|
||||
})
|
||||
|
||||
Describe("Stream", func() {
|
||||
It("returns error when id parameter is missing", func() {
|
||||
w, r := newRawReq("stream")
|
||||
_, err := router.Stream(w, r)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Download", func() {
|
||||
It("returns error when id parameter is missing", func() {
|
||||
w, r := newRawReq("download")
|
||||
_, err := router.Download(w, r)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetCoverArt", func() {
|
||||
It("handles request without error", func() {
|
||||
w, r := newRawReq("getCoverArt")
|
||||
_, err := router.GetCoverArt(w, r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetAvatar", func() {
|
||||
It("returns placeholder avatar when gravatar disabled", func() {
|
||||
w, r := newRawReq("getAvatar", "username", "admin")
|
||||
resp, err := router.GetAvatar(w, r)
|
||||
|
||||
// When gravatar is disabled, it returns nil response (writes directly to w)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp).To(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetLyrics", func() {
|
||||
It("returns empty lyrics when no match found", func() {
|
||||
r := newReq("getLyrics", "artist", "NonExistentArtist", "title", "NonExistentTitle")
|
||||
resp, err := router.GetLyrics(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.Lyrics).ToNot(BeNil())
|
||||
Expect(resp.Lyrics.Value).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetLyricsBySongId", func() {
|
||||
It("returns error when id parameter is missing", func() {
|
||||
r := newReq("getLyricsBySongId")
|
||||
_, err := router.GetLyricsBySongId(r)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns error for non-existent song id", func() {
|
||||
r := newReq("getLyricsBySongId", "id", "non-existent-id")
|
||||
_, err := router.GetLyricsBySongId(r)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
})
|
||||
312
server/e2e/subsonic_multilibrary_test.go
Normal file
312
server/e2e/subsonic_multilibrary_test.go
Normal file
@@ -0,0 +1,312 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing/fstest"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/core"
|
||||
"github.com/navidrome/navidrome/core/artwork"
|
||||
"github.com/navidrome/navidrome/core/metrics"
|
||||
"github.com/navidrome/navidrome/core/storage/storagetest"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/scanner"
|
||||
"github.com/navidrome/navidrome/server/events"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Multi-Library Support", Ordered, func() {
|
||||
var lib2 model.Library
|
||||
var adminWithLibs model.User // admin reloaded with both libraries
|
||||
var userLib1Only model.User // non-admin with lib1 access only
|
||||
|
||||
BeforeAll(func() {
|
||||
setupTestDB()
|
||||
|
||||
// Create a second FakeFS with Classical music content
|
||||
classical := template(_t{
|
||||
"albumartist": "Ludwig van Beethoven",
|
||||
"artist": "Ludwig van Beethoven",
|
||||
"album": "Symphony No. 9",
|
||||
"year": 1824,
|
||||
"genre": "Classical",
|
||||
})
|
||||
classicalFS := storagetest.FakeFS{}
|
||||
classicalFS.SetFiles(fstest.MapFS{
|
||||
"Classical/Beethoven/Symphony No. 9/01 - Allegro ma non troppo.mp3": classical(track(1, "Allegro ma non troppo")),
|
||||
"Classical/Beethoven/Symphony No. 9/02 - Ode to Joy.mp3": classical(track(2, "Ode to Joy")),
|
||||
})
|
||||
storagetest.Register("fake2", &classicalFS)
|
||||
|
||||
// Create the second library in the DB (Put auto-assigns admin users)
|
||||
lib2 = model.Library{ID: 2, Name: "Classical Library", Path: "fake2:///classical"}
|
||||
Expect(ds.Library(ctx).Put(&lib2)).To(Succeed())
|
||||
|
||||
// Reload admin user to get both libraries in the Libraries field
|
||||
loadedAdmin, err := ds.User(ctx).FindByUsername(adminUser.UserName)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
adminWithLibs = *loadedAdmin
|
||||
|
||||
// Run incremental scan to import lib2 content (lib1 files unchanged → skipped)
|
||||
s := scanner.New(ctx, ds, artwork.NoopCacheWarmer(), events.NoopBroker(),
|
||||
core.NewPlaylists(ds), metrics.NewNoopInstance())
|
||||
_, err = s.ScanAll(ctx, false)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Create a non-admin user with access only to lib1
|
||||
userLib1Only = model.User{
|
||||
ID: "multilib-user-1",
|
||||
UserName: "lib1user",
|
||||
Name: "Lib1 User",
|
||||
IsAdmin: false,
|
||||
NewPassword: "password",
|
||||
}
|
||||
Expect(ds.User(ctx).Put(&userLib1Only)).To(Succeed())
|
||||
Expect(ds.User(ctx).SetUserLibraries(userLib1Only.ID, []int{lib.ID})).To(Succeed())
|
||||
|
||||
loadedUser, err := ds.User(ctx).FindByUsername(userLib1Only.UserName)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
userLib1Only.Libraries = loadedUser.Libraries
|
||||
})
|
||||
|
||||
Describe("getMusicFolders", func() {
|
||||
It("returns both libraries for admin user", func() {
|
||||
r := newReqWithUser(adminWithLibs, "getMusicFolders")
|
||||
resp, err := router.GetMusicFolders(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.MusicFolders.Folders).To(HaveLen(2))
|
||||
|
||||
names := make([]string, len(resp.MusicFolders.Folders))
|
||||
for i, f := range resp.MusicFolders.Folders {
|
||||
names[i] = f.Name
|
||||
}
|
||||
Expect(names).To(ConsistOf("Music Library", "Classical Library"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("getArtists - library filtering", func() {
|
||||
It("returns only lib1 artists when musicFolderId=1", func() {
|
||||
r := newReqWithUser(adminWithLibs, "getArtists", "musicFolderId", fmt.Sprintf("%d", lib.ID))
|
||||
resp, err := router.GetArtists(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.Artist).ToNot(BeNil())
|
||||
|
||||
var artistNames []string
|
||||
for _, idx := range resp.Artist.Index {
|
||||
for _, a := range idx.Artists {
|
||||
artistNames = append(artistNames, a.Name)
|
||||
}
|
||||
}
|
||||
Expect(artistNames).To(ContainElements("The Beatles", "Led Zeppelin", "Miles Davis"))
|
||||
Expect(artistNames).ToNot(ContainElement("Ludwig van Beethoven"))
|
||||
})
|
||||
|
||||
It("returns only lib2 artists when musicFolderId=2", func() {
|
||||
r := newReqWithUser(adminWithLibs, "getArtists", "musicFolderId", fmt.Sprintf("%d", lib2.ID))
|
||||
resp, err := router.GetArtists(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.Artist).ToNot(BeNil())
|
||||
|
||||
var artistNames []string
|
||||
for _, idx := range resp.Artist.Index {
|
||||
for _, a := range idx.Artists {
|
||||
artistNames = append(artistNames, a.Name)
|
||||
}
|
||||
}
|
||||
Expect(artistNames).To(ContainElement("Ludwig van Beethoven"))
|
||||
Expect(artistNames).ToNot(ContainElements("The Beatles", "Led Zeppelin", "Miles Davis"))
|
||||
})
|
||||
|
||||
It("returns artists from all libraries when no musicFolderId is specified", func() {
|
||||
r := newReqWithUser(adminWithLibs, "getArtists")
|
||||
resp, err := router.GetArtists(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
|
||||
var artistNames []string
|
||||
for _, idx := range resp.Artist.Index {
|
||||
for _, a := range idx.Artists {
|
||||
artistNames = append(artistNames, a.Name)
|
||||
}
|
||||
}
|
||||
Expect(artistNames).To(ContainElements("The Beatles", "Led Zeppelin", "Miles Davis", "Ludwig van Beethoven"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("getAlbumList - library filtering", func() {
|
||||
It("returns only lib1 albums when musicFolderId=1", func() {
|
||||
w, r := newRawReqWithUser(adminWithLibs, "getAlbumList", "type", "alphabeticalByName", "musicFolderId", fmt.Sprintf("%d", lib.ID))
|
||||
resp, err := router.GetAlbumList(w, r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.AlbumList).ToNot(BeNil())
|
||||
Expect(resp.AlbumList.Album).To(HaveLen(5))
|
||||
for _, a := range resp.AlbumList.Album {
|
||||
Expect(a.Title).ToNot(Equal("Symphony No. 9"))
|
||||
}
|
||||
})
|
||||
|
||||
It("returns only lib2 albums when musicFolderId=2", func() {
|
||||
w, r := newRawReqWithUser(adminWithLibs, "getAlbumList", "type", "alphabeticalByName", "musicFolderId", fmt.Sprintf("%d", lib2.ID))
|
||||
resp, err := router.GetAlbumList(w, r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.AlbumList).ToNot(BeNil())
|
||||
Expect(resp.AlbumList.Album).To(HaveLen(1))
|
||||
Expect(resp.AlbumList.Album[0].Title).To(Equal("Symphony No. 9"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("search3 - library filtering", func() {
|
||||
It("does not find lib1 content when searching in lib2 only", func() {
|
||||
r := newReqWithUser(adminWithLibs, "search3", "query", "Beatles", "musicFolderId", fmt.Sprintf("%d", lib2.ID))
|
||||
resp, err := router.Search3(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.SearchResult3).ToNot(BeNil())
|
||||
Expect(resp.SearchResult3.Artist).To(BeEmpty())
|
||||
Expect(resp.SearchResult3.Album).To(BeEmpty())
|
||||
Expect(resp.SearchResult3.Song).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("finds lib2 content when searching in lib2", func() {
|
||||
r := newReqWithUser(adminWithLibs, "search3", "query", "Beethoven", "musicFolderId", fmt.Sprintf("%d", lib2.ID))
|
||||
resp, err := router.Search3(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.SearchResult3).ToNot(BeNil())
|
||||
Expect(resp.SearchResult3.Artist).ToNot(BeEmpty())
|
||||
Expect(resp.SearchResult3.Artist[0].Name).To(Equal("Ludwig van Beethoven"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Cross-library playlists", Ordered, func() {
|
||||
var playlistID string
|
||||
var lib1SongID, lib2SongID string
|
||||
|
||||
BeforeAll(func() {
|
||||
// Look up one song from each library
|
||||
lib1Songs, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"media_file.library_id": lib.ID},
|
||||
Max: 1, Sort: "title",
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lib1Songs).ToNot(BeEmpty())
|
||||
lib1SongID = lib1Songs[0].ID
|
||||
|
||||
lib2Songs, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"media_file.library_id": lib2.ID},
|
||||
Max: 1, Sort: "title",
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lib2Songs).ToNot(BeEmpty())
|
||||
lib2SongID = lib2Songs[0].ID
|
||||
})
|
||||
|
||||
It("admin creates a playlist with songs from both libraries", func() {
|
||||
r := newReqWithUser(adminWithLibs, "createPlaylist",
|
||||
"name", "Cross-Library Playlist", "songId", lib1SongID, "songId", lib2SongID)
|
||||
resp, err := router.CreatePlaylist(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.Playlist).ToNot(BeNil())
|
||||
Expect(resp.Playlist.SongCount).To(Equal(int32(2)))
|
||||
Expect(resp.Playlist.Entry).To(HaveLen(2))
|
||||
playlistID = resp.Playlist.Id
|
||||
})
|
||||
|
||||
It("admin makes the playlist public", func() {
|
||||
r := newReqWithUser(adminWithLibs, "updatePlaylist",
|
||||
"playlistId", playlistID, "public", "true")
|
||||
resp, err := router.UpdatePlaylist(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
})
|
||||
|
||||
It("non-admin user with lib1 only sees only lib1 tracks in the playlist", func() {
|
||||
// Reset the cached playlist repo so it's recreated with the non-admin user's context.
|
||||
// The MockDataStore caches repos on first access; resetting forces a new repo
|
||||
// whose applyLibraryFilter uses the non-admin user's library access.
|
||||
ds.MockedPlaylist = nil
|
||||
|
||||
r := newReqWithUser(userLib1Only, "getPlaylist", "id", playlistID)
|
||||
resp, err := router.GetPlaylist(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Playlist).ToNot(BeNil())
|
||||
// The playlist has 2 songs total, but the non-admin user only has access to lib1
|
||||
Expect(resp.Playlist.Entry).To(HaveLen(1))
|
||||
Expect(resp.Playlist.Entry[0].Id).To(Equal(lib1SongID))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Cross-library shares", Ordered, func() {
|
||||
var lib2AlbumID string
|
||||
|
||||
BeforeAll(func() {
|
||||
conf.Server.EnableSharing = true
|
||||
|
||||
lib2Albums, err := ds.Album(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"album.library_id": lib2.ID},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lib2Albums).ToNot(BeEmpty())
|
||||
lib2AlbumID = lib2Albums[0].ID
|
||||
})
|
||||
|
||||
It("admin creates a share for a lib2 album", func() {
|
||||
r := newReqWithUser(adminWithLibs, "createShare",
|
||||
"id", lib2AlbumID, "description", "Classical album share")
|
||||
resp, err := router.CreateShare(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.Shares).ToNot(BeNil())
|
||||
Expect(resp.Shares.Share).To(HaveLen(1))
|
||||
|
||||
share := resp.Shares.Share[0]
|
||||
Expect(share.Description).To(Equal("Classical album share"))
|
||||
Expect(share.Entry).ToNot(BeEmpty())
|
||||
Expect(share.Entry[0].Title).To(Equal("Symphony No. 9"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Library access control", func() {
|
||||
It("returns error when non-admin user requests inaccessible library", func() {
|
||||
r := newReqWithUser(userLib1Only, "getArtists", "musicFolderId", fmt.Sprintf("%d", lib2.ID))
|
||||
_, err := router.GetArtists(r)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("not found"))
|
||||
})
|
||||
|
||||
It("non-admin user sees only their library's content without musicFolderId", func() {
|
||||
r := newReqWithUser(userLib1Only, "getArtists")
|
||||
resp, err := router.GetArtists(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
|
||||
var artistNames []string
|
||||
for _, idx := range resp.Artist.Index {
|
||||
for _, a := range idx.Artists {
|
||||
artistNames = append(artistNames, a.Name)
|
||||
}
|
||||
}
|
||||
Expect(artistNames).To(ContainElements("The Beatles", "Led Zeppelin", "Miles Davis"))
|
||||
Expect(artistNames).ToNot(ContainElement("Ludwig van Beethoven"))
|
||||
})
|
||||
})
|
||||
})
|
||||
97
server/e2e/subsonic_multiuser_test.go
Normal file
97
server/e2e/subsonic_multiuser_test.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Multi-User Isolation", Ordered, func() {
|
||||
var regularUser model.User
|
||||
|
||||
BeforeAll(func() {
|
||||
setupTestDB()
|
||||
|
||||
// Create a regular (non-admin) user
|
||||
regularUser = model.User{
|
||||
ID: "regular-1",
|
||||
UserName: "regular",
|
||||
Name: "Regular User",
|
||||
IsAdmin: false,
|
||||
NewPassword: "password",
|
||||
}
|
||||
Expect(ds.User(ctx).Put(®ularUser)).To(Succeed())
|
||||
Expect(ds.User(ctx).SetUserLibraries(regularUser.ID, []int{lib.ID})).To(Succeed())
|
||||
|
||||
loadedUser, err := ds.User(ctx).FindByUsername(regularUser.UserName)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
regularUser.Libraries = loadedUser.Libraries
|
||||
})
|
||||
|
||||
Describe("Admin-only endpoint restrictions", func() {
|
||||
It("startScan fails for regular user", func() {
|
||||
r := newReqWithUser(regularUser, "startScan")
|
||||
_, err := router.StartScan(r)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("not authorized"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Browsing as regular user", func() {
|
||||
It("regular user can browse the library", func() {
|
||||
r := newReqWithUser(regularUser, "getArtists")
|
||||
resp, err := router.GetArtists(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.Artist).ToNot(BeNil())
|
||||
Expect(resp.Artist.Index).ToNot(BeEmpty())
|
||||
})
|
||||
|
||||
It("regular user can search", func() {
|
||||
r := newReqWithUser(regularUser, "search3", "query", "Beatles")
|
||||
resp, err := router.Search3(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.SearchResult3).ToNot(BeNil())
|
||||
Expect(resp.SearchResult3.Artist).ToNot(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("getUser authorization", func() {
|
||||
It("regular user can get their own info", func() {
|
||||
r := newReqWithUser(regularUser, "getUser", "username", "regular")
|
||||
resp, err := router.GetUser(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.User.Username).To(Equal("regular"))
|
||||
Expect(resp.User.AdminRole).To(BeFalse())
|
||||
})
|
||||
|
||||
It("regular user cannot get another user's info", func() {
|
||||
r := newReqWithUser(regularUser, "getUser", "username", "admin")
|
||||
_, err := router.GetUser(r)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("not authorized"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("getUsers for regular user", func() {
|
||||
It("returns only the requesting user's info", func() {
|
||||
r := newReqWithUser(regularUser, "getUsers")
|
||||
resp, err := router.GetUsers(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.Users).ToNot(BeNil())
|
||||
Expect(resp.Users.User).To(HaveLen(1))
|
||||
Expect(resp.Users.User[0].Username).To(Equal("regular"))
|
||||
Expect(resp.Users.User[0].AdminRole).To(BeFalse())
|
||||
})
|
||||
})
|
||||
})
|
||||
130
server/e2e/subsonic_playlists_test.go
Normal file
130
server/e2e/subsonic_playlists_test.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Playlist Endpoints", Ordered, func() {
|
||||
var playlistID string
|
||||
var songIDs []string
|
||||
|
||||
BeforeAll(func() {
|
||||
setupTestDB()
|
||||
|
||||
// Look up song IDs from scanned data for playlist operations
|
||||
songs, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{Sort: "title", Max: 3})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(len(songs)).To(BeNumerically(">=", 3))
|
||||
for _, s := range songs {
|
||||
songIDs = append(songIDs, s.ID)
|
||||
}
|
||||
})
|
||||
|
||||
It("getPlaylists returns empty list initially", func() {
|
||||
r := newReq("getPlaylists")
|
||||
resp, err := router.GetPlaylists(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.Playlists).ToNot(BeNil())
|
||||
Expect(resp.Playlists.Playlist).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("createPlaylist creates a new playlist with songs", func() {
|
||||
r := newReq("createPlaylist", "name", "Test Playlist", "songId", songIDs[0], "songId", songIDs[1])
|
||||
resp, err := router.CreatePlaylist(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.Playlist).ToNot(BeNil())
|
||||
Expect(resp.Playlist.Name).To(Equal("Test Playlist"))
|
||||
Expect(resp.Playlist.SongCount).To(Equal(int32(2)))
|
||||
playlistID = resp.Playlist.Id
|
||||
})
|
||||
|
||||
It("getPlaylist returns playlist with tracks", func() {
|
||||
r := newReq("getPlaylist", "id", playlistID)
|
||||
resp, err := router.GetPlaylist(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.Playlist).ToNot(BeNil())
|
||||
Expect(resp.Playlist.Name).To(Equal("Test Playlist"))
|
||||
Expect(resp.Playlist.Entry).To(HaveLen(2))
|
||||
Expect(resp.Playlist.Entry[0].Id).To(Equal(songIDs[0]))
|
||||
Expect(resp.Playlist.Entry[1].Id).To(Equal(songIDs[1]))
|
||||
})
|
||||
|
||||
It("createPlaylist without name or playlistId returns error", func() {
|
||||
r := newReq("createPlaylist", "songId", songIDs[0])
|
||||
_, err := router.CreatePlaylist(r)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("updatePlaylist can rename the playlist", func() {
|
||||
r := newReq("updatePlaylist", "playlistId", playlistID, "name", "Renamed Playlist")
|
||||
resp, err := router.UpdatePlaylist(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
|
||||
// Verify the rename
|
||||
r = newReq("getPlaylist", "id", playlistID)
|
||||
resp, err = router.GetPlaylist(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Playlist.Name).To(Equal("Renamed Playlist"))
|
||||
})
|
||||
|
||||
It("updatePlaylist can add songs", func() {
|
||||
r := newReq("updatePlaylist", "playlistId", playlistID, "songIdToAdd", songIDs[2])
|
||||
resp, err := router.UpdatePlaylist(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
|
||||
// Verify the song was added
|
||||
r = newReq("getPlaylist", "id", playlistID)
|
||||
resp, err = router.GetPlaylist(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Playlist.SongCount).To(Equal(int32(3)))
|
||||
Expect(resp.Playlist.Entry).To(HaveLen(3))
|
||||
})
|
||||
|
||||
It("updatePlaylist can remove songs by index", func() {
|
||||
// Remove the first song (index 0)
|
||||
r := newReq("updatePlaylist", "playlistId", playlistID, "songIndexToRemove", "0")
|
||||
resp, err := router.UpdatePlaylist(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
|
||||
// Verify the song was removed
|
||||
r = newReq("getPlaylist", "id", playlistID)
|
||||
resp, err = router.GetPlaylist(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Playlist.SongCount).To(Equal(int32(2)))
|
||||
Expect(resp.Playlist.Entry).To(HaveLen(2))
|
||||
})
|
||||
|
||||
It("deletePlaylist removes the playlist", func() {
|
||||
r := newReq("deletePlaylist", "id", playlistID)
|
||||
resp, err := router.DeletePlaylist(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
})
|
||||
|
||||
It("getPlaylist on deleted playlist returns error", func() {
|
||||
r := newReq("getPlaylist", "id", playlistID)
|
||||
_, err := router.GetPlaylist(r)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
94
server/e2e/subsonic_radio_test.go
Normal file
94
server/e2e/subsonic_radio_test.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Internet Radio Endpoints", Ordered, func() {
|
||||
var radioID string
|
||||
|
||||
BeforeAll(func() {
|
||||
setupTestDB()
|
||||
})
|
||||
|
||||
It("getInternetRadioStations returns empty initially", func() {
|
||||
r := newReq("getInternetRadioStations")
|
||||
resp, err := router.GetInternetRadios(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.InternetRadioStations).ToNot(BeNil())
|
||||
Expect(resp.InternetRadioStations.Radios).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("createInternetRadioStation adds a station", func() {
|
||||
r := newReq("createInternetRadioStation",
|
||||
"streamUrl", "https://stream.example.com/radio",
|
||||
"name", "Test Radio",
|
||||
"homepageUrl", "https://example.com",
|
||||
)
|
||||
resp, err := router.CreateInternetRadio(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
})
|
||||
|
||||
It("getInternetRadioStations returns the created station", func() {
|
||||
r := newReq("getInternetRadioStations")
|
||||
resp, err := router.GetInternetRadios(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.InternetRadioStations).ToNot(BeNil())
|
||||
Expect(resp.InternetRadioStations.Radios).To(HaveLen(1))
|
||||
|
||||
radio := resp.InternetRadioStations.Radios[0]
|
||||
Expect(radio.Name).To(Equal("Test Radio"))
|
||||
Expect(radio.StreamUrl).To(Equal("https://stream.example.com/radio"))
|
||||
Expect(radio.HomepageUrl).To(Equal("https://example.com"))
|
||||
radioID = radio.ID
|
||||
Expect(radioID).ToNot(BeEmpty())
|
||||
})
|
||||
|
||||
It("updateInternetRadioStation modifies the station", func() {
|
||||
r := newReq("updateInternetRadioStation",
|
||||
"id", radioID,
|
||||
"streamUrl", "https://stream.example.com/radio-v2",
|
||||
"name", "Updated Radio",
|
||||
"homepageUrl", "https://updated.example.com",
|
||||
)
|
||||
resp, err := router.UpdateInternetRadio(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
|
||||
// Verify update
|
||||
r = newReq("getInternetRadioStations")
|
||||
resp, err = router.GetInternetRadios(r)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.InternetRadioStations.Radios).To(HaveLen(1))
|
||||
Expect(resp.InternetRadioStations.Radios[0].Name).To(Equal("Updated Radio"))
|
||||
Expect(resp.InternetRadioStations.Radios[0].StreamUrl).To(Equal("https://stream.example.com/radio-v2"))
|
||||
Expect(resp.InternetRadioStations.Radios[0].HomepageUrl).To(Equal("https://updated.example.com"))
|
||||
})
|
||||
|
||||
It("deleteInternetRadioStation removes it", func() {
|
||||
r := newReq("deleteInternetRadioStation", "id", radioID)
|
||||
resp, err := router.DeleteInternetRadio(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
})
|
||||
|
||||
It("getInternetRadioStations returns empty after deletion", func() {
|
||||
r := newReq("getInternetRadioStations")
|
||||
resp, err := router.GetInternetRadios(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.InternetRadioStations).ToNot(BeNil())
|
||||
Expect(resp.InternetRadioStations.Radios).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
60
server/e2e/subsonic_scan_test.go
Normal file
60
server/e2e/subsonic_scan_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Scan Endpoints", func() {
|
||||
BeforeEach(func() {
|
||||
setupTestDB()
|
||||
})
|
||||
|
||||
It("getScanStatus returns status", func() {
|
||||
r := newReq("getScanStatus")
|
||||
resp, err := router.GetScanStatus(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.ScanStatus).ToNot(BeNil())
|
||||
Expect(resp.ScanStatus.Scanning).To(BeFalse())
|
||||
Expect(resp.ScanStatus.Count).To(BeNumerically(">", 0))
|
||||
Expect(resp.ScanStatus.LastScan).ToNot(BeNil())
|
||||
})
|
||||
|
||||
It("startScan requires admin user", func() {
|
||||
regularUser := model.User{
|
||||
ID: "user-2",
|
||||
UserName: "regular",
|
||||
Name: "Regular User",
|
||||
IsAdmin: false,
|
||||
}
|
||||
// Store the regular user in the database
|
||||
regularUserWithPass := regularUser
|
||||
regularUserWithPass.NewPassword = "password"
|
||||
Expect(ds.User(ctx).Put(®ularUserWithPass)).To(Succeed())
|
||||
Expect(ds.User(ctx).SetUserLibraries(regularUser.ID, []int{lib.ID})).To(Succeed())
|
||||
|
||||
// Reload user with libraries
|
||||
loadedUser, err := ds.User(ctx).FindByUsername(regularUser.UserName)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
regularUser.Libraries = loadedUser.Libraries
|
||||
|
||||
r := newReqWithUser(regularUser, "startScan")
|
||||
_, err = router.StartScan(r)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("not authorized"))
|
||||
})
|
||||
|
||||
It("startScan returns scan status response", func() {
|
||||
r := newReq("startScan")
|
||||
resp, err := router.StartScan(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.ScanStatus).ToNot(BeNil())
|
||||
})
|
||||
})
|
||||
158
server/e2e/subsonic_searching_test.go
Normal file
158
server/e2e/subsonic_searching_test.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Search Endpoints", func() {
|
||||
BeforeEach(func() {
|
||||
setupTestDB()
|
||||
})
|
||||
|
||||
Describe("Search2", func() {
|
||||
It("finds artists by name", func() {
|
||||
r := newReq("search2", "query", "Beatles")
|
||||
resp, err := router.Search2(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.SearchResult2).ToNot(BeNil())
|
||||
Expect(resp.SearchResult2.Artist).ToNot(BeEmpty())
|
||||
|
||||
found := false
|
||||
for _, a := range resp.SearchResult2.Artist {
|
||||
if a.Name == "The Beatles" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
Expect(found).To(BeTrue(), "expected to find artist 'The Beatles'")
|
||||
})
|
||||
|
||||
It("finds albums by name", func() {
|
||||
r := newReq("search2", "query", "Abbey Road")
|
||||
resp, err := router.Search2(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.SearchResult2).ToNot(BeNil())
|
||||
Expect(resp.SearchResult2.Album).ToNot(BeEmpty())
|
||||
|
||||
found := false
|
||||
for _, a := range resp.SearchResult2.Album {
|
||||
if a.Title == "Abbey Road" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
Expect(found).To(BeTrue(), "expected to find album 'Abbey Road'")
|
||||
})
|
||||
|
||||
It("finds songs by title", func() {
|
||||
r := newReq("search2", "query", "Come Together")
|
||||
resp, err := router.Search2(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.SearchResult2).ToNot(BeNil())
|
||||
Expect(resp.SearchResult2.Song).ToNot(BeEmpty())
|
||||
|
||||
found := false
|
||||
for _, s := range resp.SearchResult2.Song {
|
||||
if s.Title == "Come Together" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
Expect(found).To(BeTrue(), "expected to find song 'Come Together'")
|
||||
})
|
||||
|
||||
It("respects artistCount/albumCount/songCount limits", func() {
|
||||
r := newReq("search2", "query", "Beatles",
|
||||
"artistCount", "1", "albumCount", "1", "songCount", "1")
|
||||
resp, err := router.Search2(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.SearchResult2).ToNot(BeNil())
|
||||
Expect(len(resp.SearchResult2.Artist)).To(BeNumerically("<=", 1))
|
||||
Expect(len(resp.SearchResult2.Album)).To(BeNumerically("<=", 1))
|
||||
Expect(len(resp.SearchResult2.Song)).To(BeNumerically("<=", 1))
|
||||
})
|
||||
|
||||
It("supports offset parameters", func() {
|
||||
// First get all results for Beatles
|
||||
r1 := newReq("search2", "query", "Beatles", "songCount", "500")
|
||||
resp1, err := router.Search2(r1)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
allSongs := resp1.SearchResult2.Song
|
||||
|
||||
if len(allSongs) > 1 {
|
||||
// Get with offset to skip the first song
|
||||
r2 := newReq("search2", "query", "Beatles", "songOffset", "1", "songCount", "500")
|
||||
resp2, err := router.Search2(r2)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp2.SearchResult2).ToNot(BeNil())
|
||||
Expect(len(resp2.SearchResult2.Song)).To(Equal(len(allSongs) - 1))
|
||||
}
|
||||
})
|
||||
|
||||
It("returns empty results for non-matching query", func() {
|
||||
r := newReq("search2", "query", "ZZZZNONEXISTENT99999")
|
||||
resp, err := router.Search2(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.SearchResult2).ToNot(BeNil())
|
||||
Expect(resp.SearchResult2.Artist).To(BeEmpty())
|
||||
Expect(resp.SearchResult2.Album).To(BeEmpty())
|
||||
Expect(resp.SearchResult2.Song).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Search3", func() {
|
||||
It("returns results in ID3 format", func() {
|
||||
r := newReq("search3", "query", "Beatles")
|
||||
resp, err := router.Search3(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.SearchResult3).ToNot(BeNil())
|
||||
// Verify ID3 format: Artist should be ArtistID3 with Name and AlbumCount
|
||||
Expect(resp.SearchResult3.Artist).ToNot(BeEmpty())
|
||||
Expect(resp.SearchResult3.Artist[0].Name).ToNot(BeEmpty())
|
||||
Expect(resp.SearchResult3.Artist[0].Id).ToNot(BeEmpty())
|
||||
})
|
||||
|
||||
It("finds across all entity types simultaneously", func() {
|
||||
// "Beatles" should match artist, albums, and songs by The Beatles
|
||||
r := newReq("search3", "query", "Beatles")
|
||||
resp, err := router.Search3(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.SearchResult3).ToNot(BeNil())
|
||||
|
||||
// Should find at least the artist "The Beatles"
|
||||
artistFound := false
|
||||
for _, a := range resp.SearchResult3.Artist {
|
||||
if a.Name == "The Beatles" {
|
||||
artistFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
Expect(artistFound).To(BeTrue(), "expected to find artist 'The Beatles'")
|
||||
|
||||
// Should find albums by The Beatles (albums contain "Beatles" in artist field)
|
||||
// Albums are returned as AlbumID3 type
|
||||
for _, a := range resp.SearchResult3.Album {
|
||||
Expect(a.Id).ToNot(BeEmpty())
|
||||
Expect(a.Name).ToNot(BeEmpty())
|
||||
}
|
||||
|
||||
// Songs are returned as Child type
|
||||
for _, s := range resp.SearchResult3.Song {
|
||||
Expect(s.Id).ToNot(BeEmpty())
|
||||
Expect(s.Title).ToNot(BeEmpty())
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
143
server/e2e/subsonic_sharing_test.go
Normal file
143
server/e2e/subsonic_sharing_test.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Sharing Endpoints", Ordered, func() {
|
||||
var shareID string
|
||||
var albumID string
|
||||
var songID string
|
||||
|
||||
BeforeAll(func() {
|
||||
setupTestDB()
|
||||
conf.Server.EnableSharing = true
|
||||
|
||||
albums, err := ds.Album(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"album.name": "Abbey Road"},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(albums).ToNot(BeEmpty())
|
||||
albumID = albums[0].ID
|
||||
|
||||
songs, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"title": "Come Together"},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(songs).ToNot(BeEmpty())
|
||||
songID = songs[0].ID
|
||||
})
|
||||
|
||||
It("getShares returns empty initially", func() {
|
||||
r := newReq("getShares")
|
||||
resp, err := router.GetShares(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.Shares).ToNot(BeNil())
|
||||
Expect(resp.Shares.Share).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("createShare creates a share for an album", func() {
|
||||
r := newReq("createShare", "id", albumID, "description", "Check out this album")
|
||||
resp, err := router.CreateShare(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.Shares).ToNot(BeNil())
|
||||
Expect(resp.Shares.Share).To(HaveLen(1))
|
||||
|
||||
share := resp.Shares.Share[0]
|
||||
Expect(share.ID).ToNot(BeEmpty())
|
||||
Expect(share.Description).To(Equal("Check out this album"))
|
||||
Expect(share.Username).To(Equal(adminUser.UserName))
|
||||
shareID = share.ID
|
||||
})
|
||||
|
||||
It("getShares returns the created share", func() {
|
||||
r := newReq("getShares")
|
||||
resp, err := router.GetShares(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.Shares).ToNot(BeNil())
|
||||
Expect(resp.Shares.Share).To(HaveLen(1))
|
||||
|
||||
share := resp.Shares.Share[0]
|
||||
Expect(share.ID).To(Equal(shareID))
|
||||
Expect(share.Description).To(Equal("Check out this album"))
|
||||
Expect(share.Username).To(Equal(adminUser.UserName))
|
||||
Expect(share.Entry).ToNot(BeEmpty())
|
||||
})
|
||||
|
||||
It("updateShare modifies the description", func() {
|
||||
r := newReq("updateShare", "id", shareID, "description", "Updated description")
|
||||
resp, err := router.UpdateShare(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
|
||||
// Verify update
|
||||
r = newReq("getShares")
|
||||
resp, err = router.GetShares(r)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Shares.Share).To(HaveLen(1))
|
||||
Expect(resp.Shares.Share[0].Description).To(Equal("Updated description"))
|
||||
})
|
||||
|
||||
It("deleteShare removes it", func() {
|
||||
r := newReq("deleteShare", "id", shareID)
|
||||
resp, err := router.DeleteShare(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
})
|
||||
|
||||
It("getShares returns empty after deletion", func() {
|
||||
r := newReq("getShares")
|
||||
resp, err := router.GetShares(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.Shares).ToNot(BeNil())
|
||||
Expect(resp.Shares.Share).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("createShare works with a song ID", func() {
|
||||
r := newReq("createShare", "id", songID, "description", "Great song")
|
||||
resp, err := router.CreateShare(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.Shares).ToNot(BeNil())
|
||||
Expect(resp.Shares.Share).To(HaveLen(1))
|
||||
Expect(resp.Shares.Share[0].Description).To(Equal("Great song"))
|
||||
Expect(resp.Shares.Share[0].Entry).To(HaveLen(1))
|
||||
})
|
||||
|
||||
It("createShare returns error when id parameter is missing", func() {
|
||||
r := newReq("createShare")
|
||||
_, err := router.CreateShare(r)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("updateShare returns error when id parameter is missing", func() {
|
||||
r := newReq("updateShare")
|
||||
_, err := router.UpdateShare(r)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("deleteShare returns error when id parameter is missing", func() {
|
||||
r := newReq("deleteShare")
|
||||
_, err := router.DeleteShare(r)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
86
server/e2e/subsonic_system_test.go
Normal file
86
server/e2e/subsonic_system_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("System Endpoints", func() {
|
||||
BeforeEach(func() {
|
||||
setupTestDB()
|
||||
})
|
||||
|
||||
Describe("ping", func() {
|
||||
It("returns a successful response", func() {
|
||||
r := newReq("ping")
|
||||
resp, err := router.Ping(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("getLicense", func() {
|
||||
It("returns a valid license", func() {
|
||||
r := newReq("getLicense")
|
||||
resp, err := router.GetLicense(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.License).ToNot(BeNil())
|
||||
Expect(resp.License.Valid).To(BeTrue())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("getOpenSubsonicExtensions", func() {
|
||||
It("returns a list of supported extensions", func() {
|
||||
r := newReq("getOpenSubsonicExtensions")
|
||||
resp, err := router.GetOpenSubsonicExtensions(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.OpenSubsonicExtensions).ToNot(BeNil())
|
||||
Expect(*resp.OpenSubsonicExtensions).ToNot(BeEmpty())
|
||||
})
|
||||
|
||||
It("includes the transcodeOffset extension", func() {
|
||||
r := newReq("getOpenSubsonicExtensions")
|
||||
resp, err := router.GetOpenSubsonicExtensions(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
extensions := *resp.OpenSubsonicExtensions
|
||||
var names []string
|
||||
for _, ext := range extensions {
|
||||
names = append(names, ext.Name)
|
||||
}
|
||||
Expect(names).To(ContainElement("transcodeOffset"))
|
||||
})
|
||||
|
||||
It("includes the formPost extension", func() {
|
||||
r := newReq("getOpenSubsonicExtensions")
|
||||
resp, err := router.GetOpenSubsonicExtensions(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
extensions := *resp.OpenSubsonicExtensions
|
||||
var names []string
|
||||
for _, ext := range extensions {
|
||||
names = append(names, ext.Name)
|
||||
}
|
||||
Expect(names).To(ContainElement("formPost"))
|
||||
})
|
||||
|
||||
It("includes the songLyrics extension", func() {
|
||||
r := newReq("getOpenSubsonicExtensions")
|
||||
resp, err := router.GetOpenSubsonicExtensions(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
extensions := *resp.OpenSubsonicExtensions
|
||||
var names []string
|
||||
for _, ext := range extensions {
|
||||
names = append(names, ext.Name)
|
||||
}
|
||||
Expect(names).To(ContainElement("songLyrics"))
|
||||
})
|
||||
})
|
||||
})
|
||||
56
server/e2e/subsonic_users_test.go
Normal file
56
server/e2e/subsonic_users_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("User Endpoints", func() {
|
||||
BeforeEach(func() {
|
||||
setupTestDB()
|
||||
})
|
||||
|
||||
It("getUser returns current user info", func() {
|
||||
r := newReq("getUser", "username", adminUser.UserName)
|
||||
resp, err := router.GetUser(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.User).ToNot(BeNil())
|
||||
Expect(resp.User.Username).To(Equal(adminUser.UserName))
|
||||
Expect(resp.User.AdminRole).To(BeTrue())
|
||||
Expect(resp.User.StreamRole).To(BeTrue())
|
||||
Expect(resp.User.Folder).ToNot(BeEmpty())
|
||||
})
|
||||
|
||||
It("getUser with matching username case-insensitive succeeds", func() {
|
||||
r := newReq("getUser", "username", "Admin")
|
||||
resp, err := router.GetUser(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.User).ToNot(BeNil())
|
||||
Expect(resp.User.Username).To(Equal(adminUser.UserName))
|
||||
})
|
||||
|
||||
It("getUser with different username returns authorization error", func() {
|
||||
r := newReq("getUser", "username", "otheruser")
|
||||
_, err := router.GetUser(r)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("not authorized"))
|
||||
})
|
||||
|
||||
It("getUsers returns list with current user only", func() {
|
||||
r := newReq("getUsers")
|
||||
resp, err := router.GetUsers(r)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||
Expect(resp.Users).ToNot(BeNil())
|
||||
Expect(resp.Users.User).To(HaveLen(1))
|
||||
Expect(resp.Users.User[0].Username).To(Equal(adminUser.UserName))
|
||||
Expect(resp.Users.User[0].AdminRole).To(BeTrue())
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user