package jellyfin
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"time"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/conf/configtest"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/server/jellyfin/dto"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Users", func() {
var ctx context.Context
var api *Router
authedWithLibraries := func(r *http.Request, libs model.Libraries) *http.Request {
api.ds.Library().(*tests.MockLibraryRepo).SetData(libs)
stripped := make(model.Libraries, len(libs))
for i, lib := range libs {
stripped[i] = model.Library{ID: lib.ID, Name: lib.Name}
}
ctx := request.WithUser(context.Background(), model.User{ID: testID("u1"), UserName: "alice", Libraries: stripped})
return r.WithContext(ctx)
}
BeforeEach(func() {
ctx = GinkgoT().Context()
api = &Router{ds: &tests.MockDataStore{}}
})
Describe("getUserViews", func() {
It("returns one view per accessible library", func() {
libs := model.Libraries{{ID: 1, Name: "Music"}, {ID: 2, Name: "Podcasts"}}
w := httptest.NewRecorder()
api.getUserViews(w, authedWithLibraries(httptest.NewRequest("GET", "/UserViews", nil), libs))
Expect(w.Code).To(Equal(http.StatusOK))
var res dto.QueryResult
Expect(json.Unmarshal(w.Body.Bytes(), &res)).To(Succeed())
Expect(res.Items).To(HaveLen(2))
Expect(res.TotalRecordCount).To(Equal(2))
Expect(res.Items[0].Id).To(Equal(dto.EncodeLibraryID(1)))
Expect(res.Items[0].Name).To(Equal("Music"))
Expect(res.Items[0].Type).To(Equal("CollectionFolder"))
Expect(res.Items[0].CollectionType).To(Equal("music"))
Expect(res.Items[0].IsFolder).To(BeTrue())
Expect(res.Items[1].Id).To(Equal(dto.EncodeLibraryID(2)))
Expect(res.Items[1].Name).To(Equal("Podcasts"))
})
It("describes the library like Jellyfin's CollectionFolder", func() {
created := time.Date(2026, 7, 1, 12, 0, 0, 0, time.UTC)
libs := model.Libraries{{ID: 1, Name: "Music", Path: "/music", TotalAlbums: 42, CreatedAt: created}}
w := httptest.NewRecorder()
api.getUserViews(w, authedWithLibraries(httptest.NewRequest("GET", "/UserViews", nil), libs))
var res dto.QueryResult
Expect(json.Unmarshal(w.Body.Bytes(), &res)).To(Succeed())
item := res.Items[0]
Expect(*item.ChildCount).To(Equal(42))
Expect(item.DateCreated).To(Equal("2026-07-01T12:00:00.0000000Z"))
Expect(item.SortName).To(Equal("Music"))
Expect(item.Path).To(Equal("/music"))
Expect(item.LocationType).To(Equal("FileSystem"))
Expect(item.UserData).ToNot(BeNil())
Expect(item.UserData.ItemId).To(Equal(dto.EncodeLibraryID(1)))
})
It("returns a single view for a user with one library", func() {
libs := model.Libraries{{ID: 1, Name: "Music"}}
w := httptest.NewRecorder()
api.getUserViews(w, authedWithLibraries(httptest.NewRequest("GET", "/UserViews", nil), libs))
Expect(w.Code).To(Equal(http.StatusOK))
var res dto.QueryResult
Expect(json.Unmarshal(w.Body.Bytes(), &res)).To(Succeed())
Expect(res.Items).To(HaveLen(1))
Expect(res.Items[0].Id).To(Equal(dto.EncodeLibraryID(1)))
})
It("returns no views for a user with no library access", func() {
w := httptest.NewRecorder()
api.getUserViews(w, authedWithLibraries(httptest.NewRequest("GET", "/UserViews", nil), nil))
Expect(w.Code).To(Equal(http.StatusOK))
var res dto.QueryResult
Expect(json.Unmarshal(w.Body.Bytes(), &res)).To(Succeed())
Expect(res.Items).To(HaveLen(0))
Expect(res.TotalRecordCount).To(Equal(0))
})
})
It("returns the current user", func() {
w := httptest.NewRecorder()
api.getCurrentUser(w, authedWithLibraries(httptest.NewRequest("GET", "/Users/Me", nil), nil))
var u dto.UserDto
Expect(json.Unmarshal(w.Body.Bytes(), &u)).To(Succeed())
Expect(u.Name).To(Equal("alice"))
Expect(u.Policy).ToNot(BeNil())
Expect(u.Policy.IsAdministrator).To(BeFalse())
Expect(u.Configuration).ToNot(BeNil())
})
Describe("getPublicUsers", func() {
var ur *tests.MockedUserRepo
publicUsers := func() []dto.UserDto {
w := httptest.NewRecorder()
api.getPublicUsers(w, httptest.NewRequest("GET", "/Users/Public", nil))
Expect(w.Code).To(Equal(http.StatusOK))
var users []dto.UserDto
Expect(json.Unmarshal(w.Body.Bytes(), &users)).To(Succeed())
return users
}
BeforeEach(func() {
DeferCleanup(configtest.SetupConfig())
ur = api.ds.User().(*tests.MockedUserRepo)
Expect(ur.Put(ctx, &model.User{ID: testID("u1"), UserName: "alice"})).To(Succeed())
Expect(ur.Put(ctx, &model.User{ID: testID("u2"), UserName: "bob"})).To(Succeed())
})
It("returns an empty list when the config is unset", func() {
conf.Server.Jellyfin.ExposedPublicUsers = ""
Expect(publicUsers()).To(BeEmpty())
})
It("lists the configured users in order, without leaking policy", func() {
conf.Server.Jellyfin.ExposedPublicUsers = "bob, alice"
users := publicUsers()
Expect(users).To(HaveLen(2))
Expect(users[0].Name).To(Equal("bob"))
Expect(users[0].Id).To(Equal(dto.EncodeID(testID("u2"))))
Expect(users[1].Name).To(Equal("alice"))
Expect(users[0].Policy).To(BeNil())
Expect(users[0].Configuration).To(BeNil())
})
It("skips a configured username that does not exist", func() {
conf.Server.Jellyfin.ExposedPublicUsers = "alice,ghost"
users := publicUsers()
Expect(users).To(HaveLen(1))
Expect(users[0].Name).To(Equal("alice"))
})
It("matches usernames case-insensitively and de-duplicates", func() {
conf.Server.Jellyfin.ExposedPublicUsers = "ALICE, alice"
users := publicUsers()
Expect(users).To(HaveLen(1))
Expect(users[0].Name).To(Equal("alice"))
})
})
})