🏡 index : quine-global/quintodrome.git

package jellyfin

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"

	"github.com/navidrome/navidrome/core/auth"
	"github.com/navidrome/navidrome/model"
	"github.com/navidrome/navidrome/server"
	"github.com/navidrome/navidrome/server/jellyfin/dto"
	"github.com/navidrome/navidrome/tests"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var _ = Describe("AuthenticateByName", func() {
	var ctx context.Context
	var api *Router
	var ds *tests.MockDataStore
	BeforeEach(func() {
		ctx = GinkgoT().Context()
		ds = &tests.MockDataStore{}
		auth.Init(ds)
		ur := ds.User().(*tests.MockedUserRepo)
		Expect(ur.Put(ctx, &model.User{ID: testID("u1"), UserName: "alice", NewPassword: "secret"})).To(Succeed())
		api = &Router{ds: ds}
	})

	It("issues a token for valid credentials", func() {
		w := httptest.NewRecorder()
		r := httptest.NewRequest("POST", "/Users/AuthenticateByName",
			strings.NewReader(`{"Username":"alice","Pw":"secret"}`))
		api.authenticateByName(w, r)

		Expect(w.Code).To(Equal(http.StatusOK))
		var res dto.AuthenticationResult
		Expect(json.Unmarshal(w.Body.Bytes(), &res)).To(Succeed())
		Expect(res.AccessToken).ToNot(BeEmpty())
		Expect(res.User.Name).To(Equal("alice"))
		claims, err := auth.Validate(res.AccessToken)
		Expect(err).ToNot(HaveOccurred())
		Expect(claims.Subject).To(Equal("alice"))

		// Finamp reads Policy/Configuration right after login and null-crashes if they're absent.
		Expect(res.User.Policy).ToNot(BeNil())
		Expect(res.User.Policy.IsAdministrator).To(BeFalse())
		Expect(res.User.Policy.EnableAllFolders).To(BeTrue())
		Expect(res.User.Policy.EnableMediaPlayback).To(BeTrue())
		Expect(res.User.Configuration).ToNot(BeNil())
	})

	Describe("SessionInfo", func() {
		login := func(authHeader string) map[string]any {
			w := httptest.NewRecorder()
			r := httptest.NewRequest("POST", "/Users/AuthenticateByName",
				strings.NewReader(`{"Username":"alice","Pw":"secret"}`))
			r.Header.Set("Authorization", authHeader)
			api.authenticateByName(w, r)
			Expect(w.Code).To(Equal(http.StatusOK))
			var raw map[string]any
			Expect(json.Unmarshal(w.Body.Bytes(), &raw)).To(Succeed())
			Expect(raw).To(HaveKey("SessionInfo"))
			return raw["SessionInfo"].(map[string]any)
		}
		const jellybox = `MediaBrowser Client="JellyBox", Device="Mac", DeviceId="dev-1", Version="2.1"`

		It("has the fields strict clients require", func() {
			s := login(jellybox)
			Expect(s["Id"]).To(And(BeAssignableToTypeOf(""), Not(BeEmpty())))
			Expect(s["UserId"]).To(Equal(dto.EncodeID(testID("u1"))))
			Expect(s["LastActivityDate"]).To(And(BeAssignableToTypeOf(""), Not(BeEmpty())))
			for _, k := range []string{"SupportsRemoteControl", "SupportsMediaControl", "HasCustomDeviceName"} {
				Expect(s[k]).To(BeAssignableToTypeOf(false), k)
			}
			ps, ok := s["PlayState"].(map[string]any)
			Expect(ok).To(BeTrue())
			for _, k := range []string{"CanSeek", "IsPaused", "IsMuted"} {
				Expect(ps[k]).To(BeAssignableToTypeOf(false), k)
			}
		})

		It("describes the calling client", func() {
			s := login(jellybox)
			Expect(s).To(HaveKeyWithValue("UserName", "alice"))
			Expect(s).To(HaveKeyWithValue("Client", "JellyBox"))
			Expect(s).To(HaveKeyWithValue("DeviceName", "Mac"))
			Expect(s).To(HaveKeyWithValue("DeviceId", "dev-1"))
			Expect(s).To(HaveKeyWithValue("ApplicationVersion", "2.1"))
			Expect(s).To(HaveKeyWithValue("IsActive", true))
		})

		It("keeps the same Id for the same device, and a new one for another device", func() {
			first := login(jellybox)["Id"]
			Expect(login(jellybox)["Id"]).To(Equal(first))
			other := login(`MediaBrowser Client="JellyBox", Device="Mac", DeviceId="dev-2", Version="2.1"`)
			Expect(other["Id"]).ToNot(Equal(first))
		})
	})

	It("records the login time, like the web UI login does", func() {
		w := httptest.NewRecorder()
		r := httptest.NewRequest("POST", "/Users/AuthenticateByName",
			strings.NewReader(`{"Username":"alice","Pw":"secret"}`))
		api.authenticateByName(w, r)

		Expect(w.Code).To(Equal(http.StatusOK))
		ur := ds.User().(*tests.MockedUserRepo)
		usr, err := ur.FindByUsername(ctx, "alice")
		Expect(err).ToNot(HaveOccurred())
		Expect(usr.LastLoginAt).ToNot(BeNil())
	})

	It("reflects an administrator in the User.Policy", func() {
		ur := ds.User().(*tests.MockedUserRepo)
		Expect(ur.Put(ctx, &model.User{ID: testID("admin1"), UserName: "root", NewPassword: "secret", IsAdmin: true})).To(Succeed())

		w := httptest.NewRecorder()
		r := httptest.NewRequest("POST", "/Users/AuthenticateByName",
			strings.NewReader(`{"Username":"root","Pw":"secret"}`))
		api.authenticateByName(w, r)

		Expect(w.Code).To(Equal(http.StatusOK))
		var res dto.AuthenticationResult
		Expect(json.Unmarshal(w.Body.Bytes(), &res)).To(Succeed())
		Expect(res.User.Policy).ToNot(BeNil())
		Expect(res.User.Policy.IsAdministrator).To(BeTrue())
	})

	It("rejects invalid credentials with 401", func() {
		w := httptest.NewRecorder()
		r := httptest.NewRequest("POST", "/Users/AuthenticateByName",
			strings.NewReader(`{"Username":"alice","Pw":"wrong"}`))
		api.authenticateByName(w, r)
		Expect(w.Code).To(Equal(http.StatusUnauthorized))
	})

	It("rejects an empty password even for a user with an empty stored password with 401", func() {
		ur := ds.User().(*tests.MockedUserRepo)
		Expect(ur.Put(ctx, &model.User{ID: testID("e"), UserName: "empty", NewPassword: ""})).To(Succeed())

		w := httptest.NewRecorder()
		r := httptest.NewRequest("POST", "/Users/AuthenticateByName",
			strings.NewReader(`{"Username":"empty","Pw":""}`))
		api.authenticateByName(w, r)
		Expect(w.Code).To(Equal(http.StatusUnauthorized))
	})
})

var _ = Describe("AuthenticateByName body limit", func() {
	It("rejects a request body larger than the limit", func() {
		ds := &tests.MockDataStore{}
		api := &Router{ds: ds}
		w := httptest.NewRecorder()
		body := `{"Username":"alice","Pw":"secret","Padding":"` + strings.Repeat("x", server.MaxLoginBodySize) + `"}`
		r := httptest.NewRequest("POST", "/Users/AuthenticateByName", strings.NewReader(body))
		server.LimitLoginBody(http.HandlerFunc(api.authenticateByName)).ServeHTTP(w, r)
		Expect(w.Code).To(Equal(http.StatusBadRequest))
	})
})