🏡 index : quine-global/quintodrome.git

package artwork

import (
	"context"
	"os"
	"path/filepath"
	"strings"

	"github.com/navidrome/navidrome/conf"
	"github.com/navidrome/navidrome/conf/configtest"
	"github.com/navidrome/navidrome/consts"
	"github.com/navidrome/navidrome/model"
	"github.com/navidrome/navidrome/tests"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var _ = Describe("Uploader", func() {
	var ctx context.Context
	var svc Uploader
	var tmpDir string
	var artRepo *tests.MockArtworkRepo
	var queueRepo *tests.MockArtworkQueueRepo

	BeforeEach(func() {
		ctx = GinkgoT().Context()
		DeferCleanup(configtest.SetupConfig())
		tmpDir = GinkgoT().TempDir()
		conf.Server.DataFolder = conf.NewDir(tmpDir)
		artRepo = tests.CreateMockArtworkRepo()
		queueRepo = tests.CreateMockArtworkQueueRepo()
		ds := &tests.MockDataStore{MockedArtwork: artRepo, MockedArtworkQueue: queueRepo}
		svc = NewUploader(ds)
	})

	Describe("SetImage", func() {
		It("creates directory and saves image file", func() {
			reader := strings.NewReader("fake image data")
			filename, err := svc.SetImage(ctx, consts.EntityArtist, "ar-1", "Pink Floyd", "", reader, ".jpg")
			Expect(err).ToNot(HaveOccurred())
			Expect(filename).To(Equal("ar-1_pink_floyd.jpg"))

			absPath := filepath.Join(tmpDir, "artwork", "artist", "ar-1_pink_floyd.jpg")
			data, err := os.ReadFile(absPath)
			Expect(err).ToNot(HaveOccurred())
			Expect(string(data)).To(Equal("fake image data"))
		})

		It("falls back to ID-only filename when name cleans to empty", func() {
			reader := strings.NewReader("data")
			filename, err := svc.SetImage(ctx, consts.EntityPlaylist, "pl-1", "!!!", "", reader, ".png")
			Expect(err).ToNot(HaveOccurred())
			Expect(filename).To(Equal("pl-1.png"))
		})

		It("removes old image when replacing", func() {
			oldDir := filepath.Join(tmpDir, "artwork", "artist")
			Expect(os.MkdirAll(oldDir, 0755)).To(Succeed())
			oldFile := filepath.Join(oldDir, "ar-1_old.png")
			Expect(os.WriteFile(oldFile, []byte("old"), 0600)).To(Succeed())

			reader := strings.NewReader("new image")
			_, err := svc.SetImage(ctx, consts.EntityArtist, "ar-1", "New Name", oldFile, reader, ".jpg")
			Expect(err).ToNot(HaveOccurred())
			Expect(oldFile).ToNot(BeAnExistingFile())

			newPath := filepath.Join(oldDir, "ar-1_new_name.jpg")
			Expect(newPath).To(BeAnExistingFile())
		})

		It("ignores missing old file without error", func() {
			reader := strings.NewReader("data")
			_, err := svc.SetImage(ctx, consts.EntityArtist, "ar-1", "Name", "/nonexistent/path.jpg", reader, ".jpg")
			Expect(err).ToNot(HaveOccurred())
		})

		It("does not touch artwork state or the queue (that is EnqueueArtwork's job, post-Put)", func() {
			Expect(artRepo.PutItemArtwork(ctx, &model.ItemArtwork{
				ItemKind: "ar", ItemID: "ar-1", Hash: "oldhash", Source: "external",
			})).To(Succeed())

			_, err := svc.SetImage(ctx, consts.EntityArtist, "ar-1", "Pink Floyd", "", strings.NewReader("img"), ".jpg")
			Expect(err).ToNot(HaveOccurred())

			// SetImage only writes the file; the state row survives and nothing is queued until
			// the caller has persisted the new filename and called EnqueueArtwork.
			_, err = artRepo.GetItemArtwork(ctx, model.KindArtistArtwork, "ar-1", model.ImageTypePrimary)
			Expect(err).ToNot(HaveOccurred())
			Expect(queueRepo.DequeueBatch(ctx, 1000)).To(BeEmpty())
		})
	})

	Describe("EnqueueArtwork", func() {
		It("clears artwork state and enqueues a Bump", func() {
			Expect(artRepo.PutItemArtwork(ctx, &model.ItemArtwork{
				ItemKind: "ar", ItemID: "ar-1", Hash: "oldhash", Source: "external",
			})).To(Succeed())

			svc.EnqueueArtwork(ctx, consts.EntityArtist, "ar-1")

			_, err := artRepo.GetItemArtwork(ctx, model.KindArtistArtwork, "ar-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", "ar"),
				HaveField("ItemID", "ar-1"),
				HaveField("Priority", model.ArtworkPriorityBump),
			)))
		})

		It("is a no-op for an unknown entity type", func() {
			svc.EnqueueArtwork(context.Background(), "unknown", "x-1")
			Expect(queueRepo.DequeueBatch(ctx, 1000)).To(BeEmpty())
		})
	})

	Describe("RemoveImage", func() {
		It("removes the file at the given path", func() {
			dir := filepath.Join(tmpDir, "artwork", "artist")
			Expect(os.MkdirAll(dir, 0755)).To(Succeed())
			path := filepath.Join(dir, "ar-1_test.jpg")
			Expect(os.WriteFile(path, []byte("img"), 0600)).To(Succeed())

			err := svc.RemoveImage(ctx, path)
			Expect(err).ToNot(HaveOccurred())
			Expect(path).ToNot(BeAnExistingFile())
		})

		It("succeeds when file does not exist", func() {
			err := svc.RemoveImage(ctx, "/nonexistent/file.jpg")
			Expect(err).ToNot(HaveOccurred())
		})

		It("succeeds with empty path", func() {
			err := svc.RemoveImage(ctx, "")
			Expect(err).ToNot(HaveOccurred())
		})
	})
})

var _ = Describe("MaxImageUploadSize", func() {
	BeforeEach(func() {
		DeferCleanup(configtest.SetupConfig())
	})

	It("returns the configured size when valid", func() {
		conf.Server.MaxImageUploadSize = "20MB"
		Expect(MaxImageUploadSize()).To(Equal(int64(20_000_000)))
	})

	It("returns the default size when config is empty", func() {
		conf.Server.MaxImageUploadSize = ""
		Expect(MaxImageUploadSize()).To(Equal(int64(10_000_000)))
	})

	It("returns the default size when config is invalid", func() {
		conf.Server.MaxImageUploadSize = "not-a-size"
		Expect(MaxImageUploadSize()).To(Equal(int64(10_000_000)))
	})

	It("parses raw byte values", func() {
		conf.Server.MaxImageUploadSize = "52428800"
		Expect(MaxImageUploadSize()).To(Equal(int64(52_428_800)))
	})
})