package e2e
import (
"net/http"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/server/jellyfin/dto"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Multi-user access control", func() {
BeforeEach(func() { setupTestDB() })
Describe("library scoping", func() {
It("lets a library member browse its content", func() {
q := queryResult(getAs(regularUser, "/Items?IncludeItemTypes=MusicAlbum&Recursive=true"))
Expect(q.TotalRecordCount).To(Equal(5))
})
It("hides all content from a user with no library access", func() {
noAccess := model.User{ID: testID("noaccess-1"), UserName: "noaccess", Name: "No Access", NewPassword: "password"}
Expect(ds.User().Put(ctx, &noAccess)).To(Succeed())
loaded, err := ds.User().FindByUsername(ctx, "noaccess")
Expect(err).ToNot(HaveOccurred())
q := queryResult(getAs(*loaded, "/Items?IncludeItemTypes=MusicAlbum&Recursive=true"))
Expect(q.TotalRecordCount).To(Equal(0))
})
})
Describe("personal ratings", func() {
userData := func(u model.User, id string) *dto.UserItemDataDto {
var data dto.UserItemDataDto
parseInto(getAs(u, "/UserItems/"+enc(id)+"/UserData"), &data)
return &data
}
It("keeps each user's rating separate", func() {
id := songID("So What")
Expect(post("/UserItems/"+enc(id)+"/UserData", `{"Rating":10}`).Code).To(Equal(http.StatusOK))
Expect(postAs(regularUser, "/UserItems/"+enc(id)+"/Rating?likes=false", "").Code).To(Equal(http.StatusOK))
Expect(*userData(adminUser, id).Rating).To(Equal(float64(10)))
Expect(*userData(regularUser, id).Rating).To(Equal(float64(2)))
})
It("refuses writes that name another user, even for an admin, and changes nothing", func() {
id := songID("So What")
Expect(postAs(regularUser, "/UserItems/"+enc(id)+"/UserData", `{"Rating":4}`).Code).To(Equal(http.StatusOK))
Expect(postAs(regularUser, "/Users/"+enc(adminUser.ID)+"/Items/"+enc(id)+"/UserData", `{"Rating":10}`).Code).To(Equal(http.StatusForbidden))
Expect(post("/Users/"+enc(regularUser.ID)+"/Items/"+enc(id)+"/Rating?likes=true", "").Code).To(Equal(http.StatusForbidden))
Expect(del("/Users/" + enc(regularUser.ID) + "/Items/" + enc(id) + "/Rating").Code).To(Equal(http.StatusForbidden))
Expect(post("/UserItems/"+enc(id)+"/UserData?userId="+enc(regularUser.ID), `{"Rating":10}`).Code).To(Equal(http.StatusForbidden))
Expect(userData(adminUser, id).Rating).To(BeNil())
Expect(*userData(regularUser, id).Rating).To(Equal(float64(4)))
})
})
Describe("private playlists", func() {
It("does not expose another user's private playlist", func() {
adminPl := createPlaylist("Admin Private", nil)
Expect(queryResult(get("/Items?IncludeItemTypes=Playlist&Recursive=true")).TotalRecordCount).To(Equal(1))
Expect(queryResult(getAs(regularUser, "/Items?IncludeItemTypes=Playlist&Recursive=true")).TotalRecordCount).To(Equal(0))
Expect(getAs(regularUser, "/Playlists/"+enc(adminPl)+"/Items").Code).To(Equal(http.StatusNotFound))
})
It("does not let a non-owner delete another user's private playlist", func() {
adminPl := createPlaylist("Admin Private", nil)
Expect(delAs(regularUser, "/Items/"+enc(adminPl)).Code).To(Equal(http.StatusNotFound))
Expect(queryResult(get("/Items?IncludeItemTypes=Playlist&Recursive=true")).TotalRecordCount).To(Equal(1))
})
It("does not let a non-owner annotate another user's private playlist", func() {
adminPl := createPlaylist("Admin Private", nil)
Expect(postAs(regularUser, "/Users/user-1/FavoriteItems/"+enc(adminPl), "").Code).To(Equal(http.StatusNotFound))
Expect(postAs(regularUser, "/Users/user-1/Items/"+enc(adminPl)+"/Rating?Rating=10", "").Code).To(Equal(http.StatusNotFound))
Expect(postAs(regularUser, "/UserItems/"+enc(adminPl)+"/UserData", `{"Rating":10}`).Code).To(Equal(http.StatusNotFound))
})
It("lets each user manage their own playlist", func() {
regularPl := createPlaylistAs(regularUser, "Regular's Mix")
Expect(queryResult(getAs(regularUser, "/Items?IncludeItemTypes=Playlist&Recursive=true")).TotalRecordCount).To(Equal(1))
Expect(delAs(regularUser, "/Items/"+enc(regularPl)).Code).To(Equal(http.StatusNoContent))
})
})
})