mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
* refactor(req): drop redundant error return from Strings
The error from Strings carried no information beyond emptiness — it fired
exactly when the param was absent — and nearly every caller discarded it with
a blank identifier. Strings now just returns the values (empty when absent),
making the common optional-list reads one clean expression.
The few required-param callers (scrobble, createShare) check for emptiness and
return the same Subsonic error code 10 as before; their e2e tests now pin that
code. Ints and Times keep their contracts by synthesizing ErrMissingParam
themselves, so selectedMusicFolderIds is untouched. The jellyfin parseFields
helper is inlined away, since ParseFields(p.Strings("fields")...) now
compiles directly.
* docs(req): clarify Strings returns nil when param is absent
208 lines
6.2 KiB
Go
208 lines
6.2 KiB
Go
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(ctx).GetAll(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(ctx).GetAll(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(ctx).GetAll(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))
|
|
})
|
|
})
|