🏡 index : quine-global/quintodrome.git

package nativeapi

import (
	"context"
	"net/http"
	"net/http/httptest"
	"slices"
	"sync"

	"github.com/navidrome/navidrome/conf"
	"github.com/navidrome/navidrome/conf/configtest"
	"github.com/navidrome/navidrome/core/auth"
	"github.com/navidrome/navidrome/core/external"
	"github.com/navidrome/navidrome/core/playlists"
	"github.com/navidrome/navidrome/model"
	"github.com/navidrome/navidrome/server"
	"github.com/navidrome/navidrome/tests"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

type fakeProvider struct {
	external.Provider
	mu     sync.Mutex
	called []string
}

func (f *fakeProvider) RefreshInfo(_ context.Context, kind model.Kind, id string) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.called = append(f.called, kind.Prefix()+"/"+id)
	return nil
}

func (f *fakeProvider) calls() []string {
	f.mu.Lock()
	defer f.mu.Unlock()
	return slices.Clone(f.called)
}

var _ = Describe("Metadata API", func() {
	var ctx context.Context
	var ds *tests.MockDataStore
	var artRepo *tests.MockArtworkRepo
	var queueRepo *tests.MockArtworkQueueRepo
	var albumRepo *tests.MockAlbumRepo
	var provider *fakeProvider
	var router http.Handler
	var adminToken, userToken string

	BeforeEach(func() {
		ctx = GinkgoT().Context()
		DeferCleanup(configtest.SetupConfig())
		conf.Server.EnableSharing = false
		artRepo = tests.CreateMockArtworkRepo()
		queueRepo = tests.CreateMockArtworkQueueRepo()
		albumRepo = tests.CreateMockAlbumRepo()
		artistRepo := tests.CreateMockArtistRepo()
		playlistRepo := tests.CreateMockPlaylistRepo()
		Expect(albumRepo.Put(ctx, &model.Album{ID: "al-1", Name: "Kid A"})).To(Succeed())
		Expect(artistRepo.Put(ctx, &model.Artist{ID: "ar-1", Name: "Radiohead"})).To(Succeed())
		Expect(playlistRepo.Put(ctx, &model.Playlist{ID: "pl-1", Name: "My Playlist"})).To(Succeed())
		ds = &tests.MockDataStore{
			MockedArtwork:      artRepo,
			MockedArtworkQueue: queueRepo,
			MockedAlbum:        albumRepo,
			MockedArtist:       artistRepo,
			MockedPlaylist:     playlistRepo,
		}
		auth.Init(ds)
		provider = &fakeProvider{}
		nativeRouter := New(ds, nil, playlists.NewPlaylists(ds, nil), nil, tests.NewMockLibraryService(), tests.NewMockUserService(), nil, nil, nil, provider, nil)
		router = server.JWTVerifier(nativeRouter)

		adminUser := model.User{ID: "admin-1", UserName: "admin", IsAdmin: true, NewPassword: "adminpass"}
		regularUser := model.User{ID: "user-1", UserName: "regular", IsAdmin: false, NewPassword: "userpass"}
		Expect(ds.User().Put(ctx, &adminUser)).To(Succeed())
		Expect(ds.User().Put(ctx, &regularUser)).To(Succeed())

		var err error
		adminToken, err = auth.CreateToken(&adminUser)
		Expect(err).ToNot(HaveOccurred())
		userToken, err = auth.CreateToken(&regularUser)
		Expect(err).ToNot(HaveOccurred())
	})

	Describe("POST /api/metadata/{kind}/{id}/refresh", func() {
		It("clears state and enqueues a Bump for admins", func() {
			Expect(artRepo.PutItemArtwork(ctx, &model.ItemArtwork{
				ItemKind: "al", ItemID: "al-1", Hash: "oldhash", Source: "external",
			})).To(Succeed())

			req := createAuthenticatedRequest("POST", "/metadata/al/al-1/refresh", nil, adminToken)
			w := httptest.NewRecorder()
			router.ServeHTTP(w, req)

			Expect(w.Code).To(Equal(http.StatusNoContent))

			_, err := artRepo.GetItemArtwork(ctx, model.KindAlbumArtwork, "al-1", model.ImageTypePrimary)
			Expect(err).To(MatchError(model.ErrNotFound))

			queued, err := queueRepo.DequeueBatch(ctx, 1000)
			Expect(err).ToNot(HaveOccurred())
			Expect(queued).To(ContainElement(SatisfyAll(
				HaveField("ItemKind", "al"),
				HaveField("ItemID", "al-1"),
				HaveField("Priority", model.ArtworkPriorityBump),
			)))
		})

		It("returns 400 for an invalid kind", func() {
			req := createAuthenticatedRequest("POST", "/metadata/xx/id-1/refresh", nil, adminToken)
			w := httptest.NewRecorder()
			router.ServeHTTP(w, req)

			Expect(w.Code).To(Equal(http.StatusBadRequest))
		})

		It("denies access to regular users", func() {
			req := createAuthenticatedRequest("POST", "/metadata/al/al-1/refresh", nil, userToken)
			w := httptest.NewRecorder()
			router.ServeHTTP(w, req)

			Expect(w.Code).To(Equal(http.StatusForbidden))
		})

		It("denies access without authentication", func() {
			req := createUnauthenticatedRequest("POST", "/metadata/al/al-1/refresh", nil)
			w := httptest.NewRecorder()
			router.ServeHTTP(w, req)

			Expect(w.Code).To(Equal(http.StatusUnauthorized))
		})

		It("triggers an external info refresh for albums", func() {
			req := createAuthenticatedRequest("POST", "/metadata/al/al-1/refresh", nil, adminToken)
			w := httptest.NewRecorder()
			router.ServeHTTP(w, req)

			Expect(w.Code).To(Equal(http.StatusNoContent))
			Eventually(provider.calls).Should(ContainElement("al/al-1"))
		})

		It("triggers an external info refresh for artists", func() {
			req := createAuthenticatedRequest("POST", "/metadata/ar/ar-1/refresh", nil, adminToken)
			w := httptest.NewRecorder()
			router.ServeHTTP(w, req)

			Expect(w.Code).To(Equal(http.StatusNoContent))
			Eventually(provider.calls).Should(ContainElement("ar/ar-1"))
		})

		It("skips the external info refresh for kinds without external info", func() {
			req := createAuthenticatedRequest("POST", "/metadata/pl/pl-1/refresh", nil, adminToken)
			w := httptest.NewRecorder()
			router.ServeHTTP(w, req)

			Expect(w.Code).To(Equal(http.StatusNoContent))
			Consistently(provider.calls).ShouldNot(ContainElement("pl/pl-1"))
		})

		It("returns 404 for an unknown id", func() {
			req := createAuthenticatedRequest("POST", "/metadata/al/nope/refresh", nil, adminToken)
			w := httptest.NewRecorder()
			router.ServeHTTP(w, req)

			Expect(w.Code).To(Equal(http.StatusNotFound))
		})

		It("returns 500 when the lookup fails for a reason other than not-found", func() {
			albumRepo.SetError(true)

			req := createAuthenticatedRequest("POST", "/metadata/al/al-1/refresh", nil, adminToken)
			w := httptest.NewRecorder()
			router.ServeHTTP(w, req)

			Expect(w.Code).To(Equal(http.StatusInternalServerError))
		})
	})
})