navidrome/server/subsonic/album_lists_test.go
Deluan b2fec7277b refactor: optimize starred items queries with parallel execution and fix test isolation
Refactored starred items functionality by extracting common logic into getStarredItems()
method that executes artist, album, and media file queries in parallel for better performance.
This eliminates code duplication between GetStarred and GetStarred2 methods while improving
response times through concurrent database queries using run.Parallel().

Also fixed test isolation issues by adding missing auth.Init(ds) call in album lists test setup.
This resolves nil pointer dereference errors in GetStarred and GetStarred2 tests when run independently.
2025-07-16 18:10:15 -04:00

543 lines
16 KiB
Go

package subsonic
import (
"context"
"errors"
"net/http/httptest"
"github.com/navidrome/navidrome/core/auth"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/server/subsonic/responses"
"github.com/navidrome/navidrome/tests"
"github.com/navidrome/navidrome/utils/req"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Album Lists", func() {
var router *Router
var ds model.DataStore
var mockRepo *tests.MockAlbumRepo
var w *httptest.ResponseRecorder
ctx := log.NewContext(context.TODO())
BeforeEach(func() {
ds = &tests.MockDataStore{}
auth.Init(ds)
mockRepo = ds.Album(ctx).(*tests.MockAlbumRepo)
router = New(ds, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
w = httptest.NewRecorder()
})
Describe("GetAlbumList", func() {
It("should return list of the type specified", func() {
r := newGetRequest("type=newest", "offset=10", "size=20")
mockRepo.SetData(model.Albums{
{ID: "1"}, {ID: "2"},
})
resp, err := router.GetAlbumList(w, r)
Expect(err).ToNot(HaveOccurred())
Expect(resp.AlbumList.Album[0].Id).To(Equal("1"))
Expect(resp.AlbumList.Album[1].Id).To(Equal("2"))
Expect(w.Header().Get("x-total-count")).To(Equal("2"))
Expect(mockRepo.Options.Offset).To(Equal(10))
Expect(mockRepo.Options.Max).To(Equal(20))
})
It("should fail if missing type parameter", func() {
r := newGetRequest()
_, err := router.GetAlbumList(w, r)
Expect(err).To(MatchError(req.ErrMissingParam))
})
It("should return error if call fails", func() {
mockRepo.SetError(true)
r := newGetRequest("type=newest")
_, err := router.GetAlbumList(w, r)
Expect(err).To(MatchError(errSubsonic))
var subErr subError
errors.As(err, &subErr)
Expect(subErr.code).To(Equal(responses.ErrorGeneric))
})
Context("with musicFolderId parameter", func() {
var user model.User
var ctx context.Context
BeforeEach(func() {
user = model.User{
ID: "test-user",
Libraries: []model.Library{
{ID: 1, Name: "Library 1"},
{ID: 2, Name: "Library 2"},
{ID: 3, Name: "Library 3"},
},
}
ctx = request.WithUser(context.Background(), user)
})
It("should filter albums by specific library when musicFolderId is provided", func() {
r := newGetRequest("type=newest", "musicFolderId=1")
r = r.WithContext(ctx)
mockRepo.SetData(model.Albums{
{ID: "1"}, {ID: "2"},
})
resp, err := router.GetAlbumList(w, r)
Expect(err).ToNot(HaveOccurred())
Expect(resp.AlbumList.Album).To(HaveLen(2))
// Verify that library filter was applied
query, args, _ := mockRepo.Options.Filters.ToSql()
Expect(query).To(ContainSubstring("library_id IN (?)"))
Expect(args).To(ContainElement(1))
})
It("should filter albums by multiple libraries when multiple musicFolderId are provided", func() {
r := newGetRequest("type=newest", "musicFolderId=1", "musicFolderId=2")
r = r.WithContext(ctx)
mockRepo.SetData(model.Albums{
{ID: "1"}, {ID: "2"},
})
resp, err := router.GetAlbumList(w, r)
Expect(err).ToNot(HaveOccurred())
Expect(resp.AlbumList.Album).To(HaveLen(2))
// Verify that library filter was applied
query, args, _ := mockRepo.Options.Filters.ToSql()
Expect(query).To(ContainSubstring("library_id IN (?,?)"))
Expect(args).To(ContainElements(1, 2))
})
It("should return all accessible albums when no musicFolderId is provided", func() {
r := newGetRequest("type=newest")
r = r.WithContext(ctx)
mockRepo.SetData(model.Albums{
{ID: "1"}, {ID: "2"},
})
resp, err := router.GetAlbumList(w, r)
Expect(err).ToNot(HaveOccurred())
Expect(resp.AlbumList.Album).To(HaveLen(2))
// Verify that library filter was applied
query, args, _ := mockRepo.Options.Filters.ToSql()
Expect(query).To(ContainSubstring("library_id IN (?,?,?)"))
Expect(args).To(ContainElements(1, 2, 3))
})
})
})
Describe("GetAlbumList2", func() {
It("should return list of the type specified", func() {
r := newGetRequest("type=newest", "offset=10", "size=20")
mockRepo.SetData(model.Albums{
{ID: "1"}, {ID: "2"},
})
resp, err := router.GetAlbumList2(w, r)
Expect(err).ToNot(HaveOccurred())
Expect(resp.AlbumList2.Album[0].Id).To(Equal("1"))
Expect(resp.AlbumList2.Album[1].Id).To(Equal("2"))
Expect(w.Header().Get("x-total-count")).To(Equal("2"))
Expect(mockRepo.Options.Offset).To(Equal(10))
Expect(mockRepo.Options.Max).To(Equal(20))
})
It("should fail if missing type parameter", func() {
r := newGetRequest()
_, err := router.GetAlbumList2(w, r)
Expect(err).To(MatchError(req.ErrMissingParam))
})
It("should return error if call fails", func() {
mockRepo.SetError(true)
r := newGetRequest("type=newest")
_, err := router.GetAlbumList2(w, r)
Expect(err).To(MatchError(errSubsonic))
var subErr subError
errors.As(err, &subErr)
Expect(subErr.code).To(Equal(responses.ErrorGeneric))
})
Context("with musicFolderId parameter", func() {
var user model.User
var ctx context.Context
BeforeEach(func() {
user = model.User{
ID: "test-user",
Libraries: []model.Library{
{ID: 1, Name: "Library 1"},
{ID: 2, Name: "Library 2"},
{ID: 3, Name: "Library 3"},
},
}
ctx = request.WithUser(context.Background(), user)
})
It("should filter albums by specific library when musicFolderId is provided", func() {
r := newGetRequest("type=newest", "musicFolderId=1")
r = r.WithContext(ctx)
mockRepo.SetData(model.Albums{
{ID: "1"}, {ID: "2"},
})
resp, err := router.GetAlbumList2(w, r)
Expect(err).ToNot(HaveOccurred())
Expect(resp.AlbumList2.Album).To(HaveLen(2))
// Verify that library filter was applied
Expect(mockRepo.Options.Filters).ToNot(BeNil())
})
It("should filter albums by multiple libraries when multiple musicFolderId are provided", func() {
r := newGetRequest("type=newest", "musicFolderId=1", "musicFolderId=2")
r = r.WithContext(ctx)
mockRepo.SetData(model.Albums{
{ID: "1"}, {ID: "2"},
})
resp, err := router.GetAlbumList2(w, r)
Expect(err).ToNot(HaveOccurred())
Expect(resp.AlbumList2.Album).To(HaveLen(2))
// Verify that library filter was applied
Expect(mockRepo.Options.Filters).ToNot(BeNil())
})
It("should return all accessible albums when no musicFolderId is provided", func() {
r := newGetRequest("type=newest")
r = r.WithContext(ctx)
mockRepo.SetData(model.Albums{
{ID: "1"}, {ID: "2"},
})
resp, err := router.GetAlbumList2(w, r)
Expect(err).ToNot(HaveOccurred())
Expect(resp.AlbumList2.Album).To(HaveLen(2))
})
})
})
Describe("GetRandomSongs", func() {
var mockMediaFileRepo *tests.MockMediaFileRepo
BeforeEach(func() {
mockMediaFileRepo = ds.MediaFile(ctx).(*tests.MockMediaFileRepo)
})
It("should return random songs", func() {
mockMediaFileRepo.SetData(model.MediaFiles{
{ID: "1", Title: "Song 1"},
{ID: "2", Title: "Song 2"},
})
r := newGetRequest("size=2")
resp, err := router.GetRandomSongs(r)
Expect(err).ToNot(HaveOccurred())
Expect(resp.RandomSongs.Songs).To(HaveLen(2))
})
Context("with musicFolderId parameter", func() {
var user model.User
var ctx context.Context
BeforeEach(func() {
user = model.User{
ID: "test-user",
Libraries: []model.Library{
{ID: 1, Name: "Library 1"},
{ID: 2, Name: "Library 2"},
{ID: 3, Name: "Library 3"},
},
}
ctx = request.WithUser(context.Background(), user)
})
It("should filter songs by specific library when musicFolderId is provided", func() {
mockMediaFileRepo.SetData(model.MediaFiles{
{ID: "1", Title: "Song 1"},
{ID: "2", Title: "Song 2"},
})
r := newGetRequest("size=2", "musicFolderId=1")
r = r.WithContext(ctx)
resp, err := router.GetRandomSongs(r)
Expect(err).ToNot(HaveOccurred())
Expect(resp.RandomSongs.Songs).To(HaveLen(2))
// Verify that library filter was applied
query, args, _ := mockMediaFileRepo.Options.Filters.ToSql()
Expect(query).To(ContainSubstring("library_id IN (?)"))
Expect(args).To(ContainElement(1))
})
It("should filter songs by multiple libraries when multiple musicFolderId are provided", func() {
mockMediaFileRepo.SetData(model.MediaFiles{
{ID: "1", Title: "Song 1"},
{ID: "2", Title: "Song 2"},
})
r := newGetRequest("size=2", "musicFolderId=1", "musicFolderId=2")
r = r.WithContext(ctx)
resp, err := router.GetRandomSongs(r)
Expect(err).ToNot(HaveOccurred())
Expect(resp.RandomSongs.Songs).To(HaveLen(2))
// Verify that library filter was applied
query, args, _ := mockMediaFileRepo.Options.Filters.ToSql()
Expect(query).To(ContainSubstring("library_id IN (?,?)"))
Expect(args).To(ContainElements(1, 2))
})
It("should return all accessible songs when no musicFolderId is provided", func() {
mockMediaFileRepo.SetData(model.MediaFiles{
{ID: "1", Title: "Song 1"},
{ID: "2", Title: "Song 2"},
})
r := newGetRequest("size=2")
r = r.WithContext(ctx)
resp, err := router.GetRandomSongs(r)
Expect(err).ToNot(HaveOccurred())
Expect(resp.RandomSongs.Songs).To(HaveLen(2))
// Verify that library filter was applied
query, args, _ := mockMediaFileRepo.Options.Filters.ToSql()
Expect(query).To(ContainSubstring("library_id IN (?,?,?)"))
Expect(args).To(ContainElements(1, 2, 3))
})
})
})
Describe("GetSongsByGenre", func() {
var mockMediaFileRepo *tests.MockMediaFileRepo
BeforeEach(func() {
mockMediaFileRepo = ds.MediaFile(ctx).(*tests.MockMediaFileRepo)
})
It("should return songs by genre", func() {
mockMediaFileRepo.SetData(model.MediaFiles{
{ID: "1", Title: "Song 1"},
{ID: "2", Title: "Song 2"},
})
r := newGetRequest("count=2", "genre=rock")
resp, err := router.GetSongsByGenre(r)
Expect(err).ToNot(HaveOccurred())
Expect(resp.SongsByGenre.Songs).To(HaveLen(2))
})
Context("with musicFolderId parameter", func() {
var user model.User
var ctx context.Context
BeforeEach(func() {
user = model.User{
ID: "test-user",
Libraries: []model.Library{
{ID: 1, Name: "Library 1"},
{ID: 2, Name: "Library 2"},
{ID: 3, Name: "Library 3"},
},
}
ctx = request.WithUser(context.Background(), user)
})
It("should filter songs by specific library when musicFolderId is provided", func() {
mockMediaFileRepo.SetData(model.MediaFiles{
{ID: "1", Title: "Song 1"},
{ID: "2", Title: "Song 2"},
})
r := newGetRequest("count=2", "genre=rock", "musicFolderId=1")
r = r.WithContext(ctx)
resp, err := router.GetSongsByGenre(r)
Expect(err).ToNot(HaveOccurred())
Expect(resp.SongsByGenre.Songs).To(HaveLen(2))
// Verify that library filter was applied
query, args, _ := mockMediaFileRepo.Options.Filters.ToSql()
Expect(query).To(ContainSubstring("library_id IN (?)"))
Expect(args).To(ContainElement(1))
})
It("should filter songs by multiple libraries when multiple musicFolderId are provided", func() {
mockMediaFileRepo.SetData(model.MediaFiles{
{ID: "1", Title: "Song 1"},
{ID: "2", Title: "Song 2"},
})
r := newGetRequest("count=2", "genre=rock", "musicFolderId=1", "musicFolderId=2")
r = r.WithContext(ctx)
resp, err := router.GetSongsByGenre(r)
Expect(err).ToNot(HaveOccurred())
Expect(resp.SongsByGenre.Songs).To(HaveLen(2))
// Verify that library filter was applied
query, args, _ := mockMediaFileRepo.Options.Filters.ToSql()
Expect(query).To(ContainSubstring("library_id IN (?,?)"))
Expect(args).To(ContainElements(1, 2))
})
It("should return all accessible songs when no musicFolderId is provided", func() {
mockMediaFileRepo.SetData(model.MediaFiles{
{ID: "1", Title: "Song 1"},
{ID: "2", Title: "Song 2"},
})
r := newGetRequest("count=2", "genre=rock")
r = r.WithContext(ctx)
resp, err := router.GetSongsByGenre(r)
Expect(err).ToNot(HaveOccurred())
Expect(resp.SongsByGenre.Songs).To(HaveLen(2))
// Verify that library filter was applied
query, args, _ := mockMediaFileRepo.Options.Filters.ToSql()
Expect(query).To(ContainSubstring("library_id IN (?,?,?)"))
Expect(args).To(ContainElements(1, 2, 3))
})
})
})
Describe("GetStarred", func() {
var mockArtistRepo *tests.MockArtistRepo
var mockAlbumRepo *tests.MockAlbumRepo
var mockMediaFileRepo *tests.MockMediaFileRepo
BeforeEach(func() {
mockArtistRepo = ds.Artist(ctx).(*tests.MockArtistRepo)
mockAlbumRepo = ds.Album(ctx).(*tests.MockAlbumRepo)
mockMediaFileRepo = ds.MediaFile(ctx).(*tests.MockMediaFileRepo)
})
It("should return starred items", func() {
mockArtistRepo.SetData(model.Artists{{ID: "1", Name: "Artist 1"}})
mockAlbumRepo.SetData(model.Albums{{ID: "1", Name: "Album 1"}})
mockMediaFileRepo.SetData(model.MediaFiles{{ID: "1", Title: "Song 1"}})
r := newGetRequest()
resp, err := router.GetStarred(r)
Expect(err).ToNot(HaveOccurred())
Expect(resp.Starred.Artist).To(HaveLen(1))
Expect(resp.Starred.Album).To(HaveLen(1))
Expect(resp.Starred.Song).To(HaveLen(1))
})
Context("with musicFolderId parameter", func() {
var user model.User
var ctx context.Context
BeforeEach(func() {
user = model.User{
ID: "test-user",
Libraries: []model.Library{
{ID: 1, Name: "Library 1"},
{ID: 2, Name: "Library 2"},
{ID: 3, Name: "Library 3"},
},
}
ctx = request.WithUser(context.Background(), user)
})
It("should filter starred items by specific library when musicFolderId is provided", func() {
mockArtistRepo.SetData(model.Artists{{ID: "1", Name: "Artist 1"}})
mockAlbumRepo.SetData(model.Albums{{ID: "1", Name: "Album 1"}})
mockMediaFileRepo.SetData(model.MediaFiles{{ID: "1", Title: "Song 1"}})
r := newGetRequest("musicFolderId=1")
r = r.WithContext(ctx)
resp, err := router.GetStarred(r)
Expect(err).ToNot(HaveOccurred())
Expect(resp.Starred.Artist).To(HaveLen(1))
Expect(resp.Starred.Album).To(HaveLen(1))
Expect(resp.Starred.Song).To(HaveLen(1))
// Verify that library filter was applied to all types
artistQuery, artistArgs, _ := mockArtistRepo.Options.Filters.ToSql()
Expect(artistQuery).To(ContainSubstring("library_id IN (?)"))
Expect(artistArgs).To(ContainElement(1))
})
})
})
Describe("GetStarred2", func() {
var mockArtistRepo *tests.MockArtistRepo
var mockAlbumRepo *tests.MockAlbumRepo
var mockMediaFileRepo *tests.MockMediaFileRepo
BeforeEach(func() {
mockArtistRepo = ds.Artist(ctx).(*tests.MockArtistRepo)
mockAlbumRepo = ds.Album(ctx).(*tests.MockAlbumRepo)
mockMediaFileRepo = ds.MediaFile(ctx).(*tests.MockMediaFileRepo)
})
It("should return starred items in ID3 format", func() {
mockArtistRepo.SetData(model.Artists{{ID: "1", Name: "Artist 1"}})
mockAlbumRepo.SetData(model.Albums{{ID: "1", Name: "Album 1"}})
mockMediaFileRepo.SetData(model.MediaFiles{{ID: "1", Title: "Song 1"}})
r := newGetRequest()
resp, err := router.GetStarred2(r)
Expect(err).ToNot(HaveOccurred())
Expect(resp.Starred2.Artist).To(HaveLen(1))
Expect(resp.Starred2.Album).To(HaveLen(1))
Expect(resp.Starred2.Song).To(HaveLen(1))
})
Context("with musicFolderId parameter", func() {
var user model.User
var ctx context.Context
BeforeEach(func() {
user = model.User{
ID: "test-user",
Libraries: []model.Library{
{ID: 1, Name: "Library 1"},
{ID: 2, Name: "Library 2"},
{ID: 3, Name: "Library 3"},
},
}
ctx = request.WithUser(context.Background(), user)
})
It("should filter starred items by specific library when musicFolderId is provided", func() {
mockArtistRepo.SetData(model.Artists{{ID: "1", Name: "Artist 1"}})
mockAlbumRepo.SetData(model.Albums{{ID: "1", Name: "Album 1"}})
mockMediaFileRepo.SetData(model.MediaFiles{{ID: "1", Title: "Song 1"}})
r := newGetRequest("musicFolderId=1")
r = r.WithContext(ctx)
resp, err := router.GetStarred2(r)
Expect(err).ToNot(HaveOccurred())
Expect(resp.Starred2.Artist).To(HaveLen(1))
Expect(resp.Starred2.Album).To(HaveLen(1))
Expect(resp.Starred2.Song).To(HaveLen(1))
// Verify that library filter was applied to all types
artistQuery, artistArgs, _ := mockArtistRepo.Options.Filters.ToSql()
Expect(artistQuery).To(ContainSubstring("library_id IN (?)"))
Expect(artistArgs).To(ContainElement(1))
})
})
})
})