🏡 index : quine-global/quintodrome.git

package subsonic

import (
	"net/http"

	"github.com/Masterminds/squirrel"
	"github.com/navidrome/navidrome/core/auth"
	"github.com/navidrome/navidrome/model"
	"github.com/navidrome/navidrome/model/request"
	"github.com/navidrome/navidrome/tests"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var _ = Describe("Search", func() {
	var router *Router
	var ds model.DataStore
	var mockAlbumRepo *tests.MockAlbumRepo
	var mockArtistRepo *tests.MockArtistRepo
	var mockMediaFileRepo *tests.MockMediaFileRepo

	BeforeEach(func() {
		ds = &tests.MockDataStore{}
		auth.Init(ds)

		router = New(ds, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)

		// Get references to the mock repositories so we can inspect their Options
		mockAlbumRepo = ds.Album().(*tests.MockAlbumRepo)
		mockArtistRepo = ds.Artist().(*tests.MockArtistRepo)
		mockMediaFileRepo = ds.MediaFile().(*tests.MockMediaFileRepo)
	})

	Context("musicFolderId parameter", func() {
		assertQueryOptions := func(filter squirrel.Sqlizer, expectedQuery string, expectedArgs ...any) {
			GinkgoHelper()
			query, args, err := filter.ToSql()
			Expect(err).ToNot(HaveOccurred())
			Expect(query).To(ContainSubstring(expectedQuery))
			Expect(args).To(ContainElements(expectedArgs...))
		}

		Describe("Search2", func() {
			It("scopes all entity types to the requested libraries", func() {
				// The subsonic layer passes the same library_id filter to all three repos; the
				// artist repository translates it to the join-free library_artist predicate itself.
				r := newGetRequest("query=test", "musicFolderId=1")
				ctx := request.WithUser(r.Context(), model.User{
					ID:       "user1",
					UserName: "testuser",
					Libraries: []model.Library{
						{ID: 1, Name: "Library 1"},
						{ID: 2, Name: "Library 2"},
					},
				})
				r = r.WithContext(ctx)

				resp, err := router.Search2(r)

				Expect(err).ToNot(HaveOccurred())
				Expect(resp).ToNot(BeNil())
				Expect(resp.SearchResult2).ToNot(BeNil())

				assertQueryOptions(mockAlbumRepo.Options.Filters, "library_id IN (?)", 1)
				assertQueryOptions(mockMediaFileRepo.Options.Filters, "library_id IN (?)", 1)
				assertQueryOptions(mockArtistRepo.Options.Filters, "library_id IN (?)", 1)
			})

			It("applies no library filter when musicFolderId is not provided", func() {
				r := newGetRequest("query=test") // no musicFolderId → all accessible libraries
				ctx := request.WithUser(r.Context(), model.User{
					ID:       "user1",
					UserName: "testuser",
					Libraries: []model.Library{
						{ID: 1, Name: "Library 1"},
						{ID: 2, Name: "Library 2"},
						{ID: 3, Name: "Library 3"},
					},
				})
				r = r.WithContext(ctx)

				resp, err := router.Search2(r)

				Expect(err).ToNot(HaveOccurred())
				Expect(resp).ToNot(BeNil())
				Expect(resp.SearchResult2).ToNot(BeNil())

				assertQueryOptions(mockAlbumRepo.Options.Filters, "library_id IN (?,?,?)", 1, 2, 3)
				assertQueryOptions(mockMediaFileRepo.Options.Filters, "library_id IN (?,?,?)", 1, 2, 3)
				assertQueryOptions(mockArtistRepo.Options.Filters, "library_id IN (?,?,?)", 1, 2, 3)
			})

			It("should return empty results when user has no accessible libraries", func() {
				r := newGetRequest("query=test")
				ctx := request.WithUser(r.Context(), model.User{
					ID:        "user1",
					UserName:  "testuser",
					Libraries: []model.Library{}, // No libraries
				})
				r = r.WithContext(ctx)

				resp, err := router.Search2(r)

				Expect(err).ToNot(HaveOccurred())
				Expect(resp).ToNot(BeNil())
				Expect(resp.SearchResult2).ToNot(BeNil())
				Expect(mockAlbumRepo.Options.Filters).To(BeNil())
				Expect(mockArtistRepo.Options.Filters).To(BeNil())
				Expect(mockMediaFileRepo.Options.Filters).To(BeNil())
			})

			It("should return error for inaccessible musicFolderId", func() {
				r := newGetRequest("query=test", "musicFolderId=999")
				ctx := request.WithUser(r.Context(), model.User{
					ID:        "user1",
					UserName:  "testuser",
					Libraries: []model.Library{{ID: 1, Name: "Library 1"}},
				})
				r = r.WithContext(ctx)

				resp, err := router.Search2(r)

				Expect(err).To(HaveOccurred())
				Expect(err.Error()).To(ContainSubstring("Library 999 not found or not accessible"))
				Expect(resp).To(BeNil())
			})
		})

		Describe("Search3", func() {
			It("scopes all entity types to the requested libraries", func() {
				r := newGetRequest("query=test", "musicFolderId=1")
				ctx := request.WithUser(r.Context(), model.User{
					ID:       "user1",
					UserName: "testuser",
					Libraries: []model.Library{
						{ID: 1, Name: "Library 1"},
						{ID: 2, Name: "Library 2"},
					},
				})
				r = r.WithContext(ctx)

				resp, err := router.Search3(r)

				Expect(err).ToNot(HaveOccurred())
				Expect(resp).ToNot(BeNil())
				Expect(resp.SearchResult3).ToNot(BeNil())

				assertQueryOptions(mockAlbumRepo.Options.Filters, "library_id IN (?)", 1)
				assertQueryOptions(mockMediaFileRepo.Options.Filters, "library_id IN (?)", 1)
				assertQueryOptions(mockArtistRepo.Options.Filters, "library_id IN (?)", 1)
			})

			It("applies no library filter when musicFolderId is not provided", func() {
				r := newGetRequest("query=test") // no musicFolderId → all accessible libraries
				ctx := request.WithUser(r.Context(), model.User{
					ID:       "user1",
					UserName: "testuser",
					Libraries: []model.Library{
						{ID: 1, Name: "Library 1"},
						{ID: 2, Name: "Library 2"},
						{ID: 3, Name: "Library 3"},
					},
				})
				r = r.WithContext(ctx)

				resp, err := router.Search3(r)

				Expect(err).ToNot(HaveOccurred())
				Expect(resp).ToNot(BeNil())
				Expect(resp.SearchResult3).ToNot(BeNil())

				assertQueryOptions(mockAlbumRepo.Options.Filters, "library_id IN (?,?,?)", 1, 2, 3)
				assertQueryOptions(mockMediaFileRepo.Options.Filters, "library_id IN (?,?,?)", 1, 2, 3)
				assertQueryOptions(mockArtistRepo.Options.Filters, "library_id IN (?,?,?)", 1, 2, 3)
			})

			It("should return empty results when user has no accessible libraries", func() {
				r := newGetRequest("query=test")
				ctx := request.WithUser(r.Context(), model.User{
					ID:        "user1",
					UserName:  "testuser",
					Libraries: []model.Library{}, // No libraries
				})
				r = r.WithContext(ctx)

				resp, err := router.Search3(r)

				Expect(err).ToNot(HaveOccurred())
				Expect(resp).ToNot(BeNil())
				Expect(resp.SearchResult3).ToNot(BeNil())
				Expect(mockAlbumRepo.Options.Filters).To(BeNil())
				Expect(mockArtistRepo.Options.Filters).To(BeNil())
				Expect(mockMediaFileRepo.Options.Filters).To(BeNil())
			})

			It("should return error for inaccessible musicFolderId", func() {
				// Test that the endpoint returns an error when user tries to access a library they don't have access to
				r := newGetRequest("query=test", "musicFolderId=999")
				ctx := request.WithUser(r.Context(), model.User{
					ID:        "user1",
					UserName:  "testuser",
					Libraries: []model.Library{{ID: 1, Name: "Library 1"}},
				})
				r = r.WithContext(ctx)

				resp, err := router.Search3(r)

				Expect(err).To(HaveOccurred())
				Expect(err.Error()).To(ContainSubstring("Library 999 not found or not accessible"))
				Expect(resp).To(BeNil())
			})
		})
	})

	Describe("artwork emission", func() {
		newSearchRequest := func() *http.Request {
			r := newGetRequest("query=test")
			ctx := request.WithUser(r.Context(), model.User{ID: "user1", Libraries: []model.Library{{ID: 1}}})
			return r.WithContext(ctx)
		}

		It("omits coverArt and imageUrl for a known-absent artist", func() {
			mockArtistRepo.SetData(model.Artists{
				{ID: "ar-1", Name: "Absent Artist", ItemImage: model.ItemImage{ImageAbsent: true}},
			})
			resp, err := router.Search2(newSearchRequest())
			Expect(err).ToNot(HaveOccurred())
			Expect(resp.SearchResult2.Artist).To(HaveLen(1))
			Expect(resp.SearchResult2.Artist[0].CoverArt).To(BeEmpty())
			Expect(resp.SearchResult2.Artist[0].ArtistImageUrl).To(BeEmpty())
		})

		It("emits coverArt and imageUrl for an unresolved artist", func() {
			mockArtistRepo.SetData(model.Artists{{ID: "ar-1", Name: "Unresolved Artist"}})
			resp, err := router.Search2(newSearchRequest())
			Expect(err).ToNot(HaveOccurred())
			Expect(resp.SearchResult2.Artist).To(HaveLen(1))
			Expect(resp.SearchResult2.Artist[0].CoverArt).To(Equal("ar-ar-1"))
			Expect(resp.SearchResult2.Artist[0].ArtistImageUrl).ToNot(BeEmpty())
		})
	})
})