package auth_testimport ("time""github.com/go-chi/jwtauth/v5""github.com/navidrome/navidrome/core/auth". "github.com/onsi/ginkgo/v2". "github.com/onsi/gomega")var _ = Describe("Claims", func() {Describe("ToMap", func() {It("includes only non-zero fields", func() {c := auth.Claims{Issuer: "ND",Subject: "johndoe",UserID: "123",IsAdmin: true,}m := c.ToMap()Expect(m).To(HaveKeyWithValue("iss", "ND"))Expect(m).To(HaveKeyWithValue("sub", "johndoe"))Expect(m).To(HaveKeyWithValue("uid", "123"))Expect(m).To(HaveKeyWithValue("adm", true))Expect(m).NotTo(HaveKey("exp"))Expect(m).NotTo(HaveKey("iat"))Expect(m).NotTo(HaveKey("id"))Expect(m).NotTo(HaveKey("f"))Expect(m).NotTo(HaveKey("b"))Expect(m).NotTo(HaveKey("sid"))})It("includes expiration and issued-at when set", func() {now := time.Now()c := auth.Claims{IssuedAt: now,ExpiresAt: now.Add(time.Hour),}m := c.ToMap()Expect(m).To(HaveKey("iat"))Expect(m).To(HaveKey("exp"))})It("includes custom claims for public tokens", func() {c := auth.Claims{ID: "al-123",Format: "mp3",BitRate: 192,}m := c.ToMap()Expect(m).To(HaveKeyWithValue("id", "al-123"))Expect(m).To(HaveKeyWithValue("f", "mp3"))Expect(m).To(HaveKeyWithValue("b", 192))})It("includes share ID claim when set", func() {c := auth.Claims{ShareID: "abc1234567"}m := c.ToMap()Expect(m).To(HaveKeyWithValue("sid", "abc1234567"))})})Describe("ClaimsFromToken", func() {It("round-trips session claims through encode/decode", func() {tokenAuth := jwtauth.New("HS256", []byte("test-secret"), nil)now := time.Now().Truncate(time.Second)original := auth.Claims{Issuer: "ND",Subject: "johndoe",UserID: "123",IsAdmin: true,}m := original.ToMap()m["iat"] = now.UTC().Unix()token, _, err := tokenAuth.Encode(m)Expect(err).NotTo(HaveOccurred())c := auth.ClaimsFromToken(token)Expect(c.Issuer).To(Equal("ND"))Expect(c.Subject).To(Equal("johndoe"))Expect(c.UserID).To(Equal("123"))Expect(c.IsAdmin).To(BeTrue())Expect(c.IssuedAt.UTC()).To(Equal(now.UTC()))})It("round-trips public token claims through encode/decode", func() {tokenAuth := jwtauth.New("HS256", []byte("test-secret"), nil)original := auth.Claims{Issuer: "ND",ID: "al-456",Format: "opus",BitRate: 128,ShareID: "abc1234567",}token, _, err := tokenAuth.Encode(original.ToMap())Expect(err).NotTo(HaveOccurred())c := auth.ClaimsFromToken(token)Expect(c.Issuer).To(Equal("ND"))Expect(c.ID).To(Equal("al-456"))Expect(c.ShareID).To(Equal("abc1234567"))Expect(c.Format).To(Equal("opus"))Expect(c.BitRate).To(Equal(128))})})Describe("Audience and Epoch claims", func() {It("omits both when zero", func() {m := auth.Claims{ID: "artwork-id"}.ToMap()Expect(m).ToNot(HaveKey("aud"))Expect(m).ToNot(HaveKey("ep"))})It("includes them when set", func() {m := auth.Claims{Subject: "u", Epoch: 3, Audience: []string{"jellyfin"}}.ToMap()Expect(m).To(HaveKeyWithValue("ep", 3))Expect(m).To(HaveKeyWithValue("aud", []string{"jellyfin"}))})It("round-trips through a signed token", func() {tokenAuth := jwtauth.New("HS256", []byte("test-secret"), nil)_, tokenStr, err := tokenAuth.Encode(auth.Claims{Subject: "u", Epoch: 7, Audience: []string{"jellyfin"},}.ToMap())Expect(err).ToNot(HaveOccurred())token, err := jwtauth.VerifyToken(tokenAuth, tokenStr)Expect(err).ToNot(HaveOccurred())claims := auth.ClaimsFromToken(token)Expect(claims.Epoch).To(Equal(7))Expect(claims.Audience).To(Equal([]string{"jellyfin"}))})It("reads a token that has neither claim", func() {tokenAuth := jwtauth.New("HS256", []byte("test-secret"), nil)_, tokenStr, err := tokenAuth.Encode(auth.Claims{Subject: "u"}.ToMap())Expect(err).ToNot(HaveOccurred())token, err := jwtauth.VerifyToken(tokenAuth, tokenStr)Expect(err).ToNot(HaveOccurred())claims := auth.ClaimsFromToken(token)Expect(claims.Epoch).To(BeZero())Expect(claims.Audience).To(BeEmpty())})})})