🏡 index : quine-global/quintodrome.git

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() {
		conf.Server.EnableSharing = true
		setupTestDB()

		albums, err := ds.Album().GetAll(ctx, 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().GetAll(ctx, 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() {
		resp := doReq("getShares")

		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() {
		resp := doReq("createShare", "id", albumID, "description", "Check out this album")

		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() {
		resp := doReq("getShares")

		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() {
		resp := doReq("updateShare", "id", shareID, "description", "Updated description")

		Expect(resp.Status).To(Equal(responses.StatusOK))

		// Verify update
		resp = doReq("getShares")
		Expect(resp.Shares.Share).To(HaveLen(1))
		Expect(resp.Shares.Share[0].Description).To(Equal("Updated description"))
	})

	It("deleteShare removes it", func() {
		resp := doReq("deleteShare", "id", shareID)

		Expect(resp.Status).To(Equal(responses.StatusOK))
	})

	It("getShares returns empty after deletion", func() {
		resp := doReq("getShares")

		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() {
		resp := doReq("createShare", "id", songID, "description", "Great song")

		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() {
		resp := doReq("createShare")

		Expect(resp.Status).To(Equal(responses.StatusFailed))
		Expect(resp.Error).ToNot(BeNil())
		Expect(resp.Error.Code).To(Equal(responses.ErrorMissingParameter))
	})

	It("updateShare returns error when id parameter is missing", func() {
		resp := doReq("updateShare")

		Expect(resp.Status).To(Equal(responses.StatusFailed))
		Expect(resp.Error).ToNot(BeNil())
	})

	It("deleteShare returns error when id parameter is missing", func() {
		resp := doReq("deleteShare")

		Expect(resp.Status).To(Equal(responses.StatusFailed))
		Expect(resp.Error).ToNot(BeNil())
	})
})

var _ = Describe("Sharing Cross-User Isolation", Ordered, func() {
	var userA, userB model.User
	var shareID string
	var albumID string

	BeforeAll(func() {
		conf.Server.EnableSharing = true
		setupTestDB()

		userA = createUser("share-user-a", "share-user-a", "Share User A", false)
		userB = createUser("share-user-b", "share-user-b", "Share User B", false)

		albums, err := ds.Album().GetAll(ctx, model.QueryOptions{
			Filters: squirrel.Eq{"album.name": "Abbey Road"},
		})
		Expect(err).ToNot(HaveOccurred())
		Expect(albums).ToNot(BeEmpty())
		albumID = albums[0].ID

		resp := doReqWithUser(userA, "createShare", "id", albumID, "description", "User A's share")
		Expect(resp.Status).To(Equal(responses.StatusOK))
		Expect(resp.Shares.Share).To(HaveLen(1))
		shareID = resp.Shares.Share[0].ID
		Expect(resp.Shares.Share[0].Username).To(Equal(userA.UserName))
	})

	It("userB's getShares does not leak userA's share", func() {
		resp := doReqWithUser(userB, "getShares")

		Expect(resp.Status).To(Equal(responses.StatusOK))
		Expect(resp.Shares).ToNot(BeNil())
		Expect(resp.Shares.Share).To(BeEmpty())
	})

	It("userA still sees own share", func() {
		resp := doReqWithUser(userA, "getShares")

		Expect(resp.Status).To(Equal(responses.StatusOK))
		Expect(resp.Shares.Share).To(HaveLen(1))
		Expect(resp.Shares.Share[0].ID).To(Equal(shareID))
		Expect(resp.Shares.Share[0].Description).To(Equal("User A's share"))
	})

	It("admin sees userA's share", func() {
		resp := doReqWithUser(adminUser, "getShares")

		Expect(resp.Status).To(Equal(responses.StatusOK))
		ids := make([]string, len(resp.Shares.Share))
		for i, s := range resp.Shares.Share {
			ids[i] = s.ID
		}
		Expect(ids).To(ContainElement(shareID))
	})

	It("userB cannot updateShare on userA's share", func() {
		resp := doReqWithUser(userB, "updateShare", "id", shareID, "description", "hijacked")

		Expect(resp.Status).To(Equal(responses.StatusFailed))
		Expect(resp.Error).ToNot(BeNil())

		// Confirm description unchanged for userA.
		check := doReqWithUser(userA, "getShares")
		Expect(check.Shares.Share).To(HaveLen(1))
		Expect(check.Shares.Share[0].Description).To(Equal("User A's share"))
	})

	It("userB cannot deleteShare on userA's share", func() {
		resp := doReqWithUser(userB, "deleteShare", "id", shareID)

		Expect(resp.Status).To(Equal(responses.StatusFailed))
		Expect(resp.Error).ToNot(BeNil())

		// Confirm share still present for userA.
		check := doReqWithUser(userA, "getShares")
		Expect(check.Shares.Share).To(HaveLen(1))
		Expect(check.Shares.Share[0].ID).To(Equal(shareID))
	})
})

var _ = Describe("Sharing Downloadable Default", func() {
	var albumID string

	BeforeEach(func() {
		conf.Server.EnableSharing = true
		setupTestDB()
		conf.Server.EnableDownloads = true
		albumID = albumIDByName("Abbey Road")
	})

	createShare := func(params ...string) *model.Share {
		GinkgoHelper()
		resp := doReq("createShare", append([]string{"id", albumID}, params...)...)
		Expect(resp.Status).To(Equal(responses.StatusOK))
		Expect(resp.Shares.Share).To(HaveLen(1))
		share, err := ds.Share().Get(ctx, resp.Shares.Share[0].ID)
		Expect(err).ToNot(HaveOccurred())
		return share
	}

	DescribeTable("createShare resolves downloadable",
		func(defaultDownloadable, enableDownloads bool, params []string, expected bool) {
			conf.Server.DefaultDownloadableShare = defaultDownloadable
			conf.Server.EnableDownloads = enableDownloads

			Expect(createShare(params...).Downloadable).To(Equal(expected))
		},
		Entry("applies the default when the param is absent", true, true, nil, true),
		Entry("stays off when the default is off", false, true, nil, false),
		Entry("ignores the default when downloads are disabled", true, false, nil, false),
		Entry("honors an explicit false over the default", true, true, []string{"downloadable", "false"}, false),
		Entry("honors an explicit true over the default", false, true, []string{"downloadable", "true"}, true),
	)

	It("updateShare keeps the current downloadable when the param is absent", func() {
		conf.Server.DefaultDownloadableShare = true
		share := createShare()
		Expect(share.Downloadable).To(BeTrue())

		resp := doReq("updateShare", "id", share.ID, "description", "Updated")
		Expect(resp.Status).To(Equal(responses.StatusOK))

		updated, err := ds.Share().Get(ctx, share.ID)
		Expect(err).ToNot(HaveOccurred())
		Expect(updated.Description).To(Equal("Updated"))
		Expect(updated.Downloadable).To(BeTrue())
	})

	It("updateShare applies an explicit downloadable and keeps the description", func() {
		conf.Server.DefaultDownloadableShare = true
		share := createShare("description", "Keep me")

		resp := doReq("updateShare", "id", share.ID, "downloadable", "false")
		Expect(resp.Status).To(Equal(responses.StatusOK))

		updated, err := ds.Share().Get(ctx, share.ID)
		Expect(err).ToNot(HaveOccurred())
		Expect(updated.Downloadable).To(BeFalse())
		Expect(updated.Description).To(Equal("Keep me"))
	})

	It("updateShare clears the description when it is sent empty", func() {
		share := createShare("description", "Clear me")

		resp := doReq("updateShare", "id", share.ID, "description", "")
		Expect(resp.Status).To(Equal(responses.StatusOK))

		updated, err := ds.Share().Get(ctx, share.ID)
		Expect(err).ToNot(HaveOccurred())
		Expect(updated.Description).To(BeEmpty())
	})
})