quintodrome/server/public/handle_streams_test.go
Deluan efe9291db0 refactor: multiple syntax updates for Go 1.26
Signed-off-by: Deluan <deluan@navidrome.org>
2026-05-19 18:02:36 -03:00

194 lines
6.2 KiB
Go

package public
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"time"
"github.com/go-chi/jwtauth/v5"
"github.com/navidrome/navidrome/core/auth"
"github.com/navidrome/navidrome/core/stream"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
type mockStreamer struct {
req stream.Request
called bool
}
func (m *mockStreamer) NewStream(_ context.Context, _ *model.MediaFile, r stream.Request) (*stream.Stream, error) {
m.called = true
m.req = r
return nil, errors.New("mock: not implemented")
}
var _ = Describe("decodeStreamInfo", func() {
BeforeEach(func() {
auth.TokenAuth = jwtauth.New("HS256", []byte("test-secret"), nil)
})
It("decodes a valid token with all fields", func() {
claims := auth.Claims{ID: "mf-123", Format: "mp3", BitRate: 192, ShareID: "share123"}
token, _ := auth.CreateExpiringPublicToken(time.Now().Add(time.Hour), claims)
info, err := decodeStreamInfo(token)
Expect(err).NotTo(HaveOccurred())
Expect(info.id).To(Equal("mf-123"))
Expect(info.format).To(Equal("mp3"))
Expect(info.bitrate).To(Equal(192))
Expect(info.shareID).To(Equal("share123"))
})
It("rejects an expired token", func() {
claims := auth.Claims{ID: "mf-123", ShareID: "share123"}
token, _ := auth.CreateExpiringPublicToken(time.Now().Add(-time.Hour), claims)
_, err := decodeStreamInfo(token)
Expect(err).To(HaveOccurred())
})
It("accepts a token without exp (non-expiring share)", func() {
claims := auth.Claims{ID: "mf-123", ShareID: "share123"}
token, _ := auth.CreatePublicToken(claims)
info, err := decodeStreamInfo(token)
Expect(err).NotTo(HaveOccurred())
Expect(info.id).To(Equal("mf-123"))
Expect(info.shareID).To(Equal("share123"))
})
It("rejects a token without an id claim", func() {
claims := auth.Claims{ShareID: "share123"}
token, _ := auth.CreatePublicToken(claims)
_, err := decodeStreamInfo(token)
Expect(err).To(HaveOccurred())
})
It("rejects an invalid token string", func() {
_, err := decodeStreamInfo("not-a-valid-token")
Expect(err).To(HaveOccurred())
})
It("handles tokens without shareID (backward compat)", func() {
claims := auth.Claims{ID: "mf-123", Format: "opus"}
token, _ := auth.CreatePublicToken(claims)
info, err := decodeStreamInfo(token)
Expect(err).NotTo(HaveOccurred())
Expect(info.id).To(Equal("mf-123"))
Expect(info.format).To(Equal("opus"))
Expect(info.shareID).To(BeEmpty())
})
})
var _ = Describe("encodeMediafileShare", func() {
BeforeEach(func() {
auth.TokenAuth = jwtauth.New("HS256", []byte("test-secret"), nil)
})
It("includes the share ID in the token", func() {
exp := new(time.Now().Add(time.Hour))
s := model.Share{ID: "shareABC", Format: "mp3", MaxBitRate: 320, ExpiresAt: exp}
token := encodeMediafileShare(s, "mf-999")
info, err := decodeStreamInfo(token)
Expect(err).NotTo(HaveOccurred())
Expect(info.shareID).To(Equal("shareABC"))
Expect(info.id).To(Equal("mf-999"))
Expect(info.format).To(Equal("mp3"))
Expect(info.bitrate).To(Equal(320))
})
It("creates a non-expiring token when share has no expiry", func() {
s := model.Share{ID: "shareXYZ", ExpiresAt: nil}
token := encodeMediafileShare(s, "mf-111")
info, err := decodeStreamInfo(token)
Expect(err).NotTo(HaveOccurred())
Expect(info.shareID).To(Equal("shareXYZ"))
Expect(info.id).To(Equal("mf-111"))
})
})
var _ = Describe("handleStream", func() {
var ds *tests.MockDataStore
var shareRepo *tests.MockShareRepo
var streamer *mockStreamer
var pub *Router
BeforeEach(func() {
auth.TokenAuth = jwtauth.New("HS256", []byte("test-secret"), nil)
ds = &tests.MockDataStore{}
shareRepo = &tests.MockShareRepo{}
ds.MockedShare = shareRepo
streamer = &mockStreamer{}
pub = &Router{ds: ds, streamer: streamer}
})
makeRequest := func(token string) *httptest.ResponseRecorder {
r := httptest.NewRequest("GET", "/public/s/token?%3Aid="+token, nil)
w := httptest.NewRecorder()
pub.handleStream(w, r)
return w
}
It("passes all validation and reaches the streamer for a valid token", func() {
shareRepo.ID = "share123"
mfRepo := tests.CreateMockMediaFileRepo()
mfRepo.SetData(model.MediaFiles{{ID: "mf-123", Title: "Test Song"}})
ds.MockedMediaFile = mfRepo
claims := auth.Claims{ID: "mf-123", Format: "mp3", BitRate: 192, ShareID: "share123"}
token, _ := auth.CreateExpiringPublicToken(time.Now().Add(time.Hour), claims)
makeRequest(token)
Expect(streamer.called).To(BeTrue())
Expect(streamer.req.Format).To(Equal("mp3"))
Expect(streamer.req.BitRate).To(Equal(192))
})
It("returns 400 for an expired token", func() {
claims := auth.Claims{ID: "mf-123", ShareID: "share123"}
token, _ := auth.CreateExpiringPublicToken(time.Now().Add(-time.Hour), claims)
w := makeRequest(token)
Expect(w.Code).To(Equal(http.StatusBadRequest))
})
It("returns 404 when share has been deleted", func() {
shareRepo.ID = "other-share"
claims := auth.Claims{ID: "mf-123", ShareID: "deleted-share"}
token, _ := auth.CreateExpiringPublicToken(time.Now().Add(time.Hour), claims)
w := makeRequest(token)
Expect(w.Code).To(Equal(http.StatusNotFound))
})
It("returns 410 when share has been set to expired", func() {
shareRepo.ID = "share123"
shareRepo.Entity = &model.Share{ID: "share123", ExpiresAt: new(time.Now().Add(-time.Hour))}
claims := auth.Claims{ID: "mf-123", ShareID: "share123"}
token, _ := auth.CreatePublicToken(claims)
w := makeRequest(token)
Expect(w.Code).To(Equal(http.StatusGone))
})
It("returns 500 when share lookup fails", func() {
shareRepo.Error = errors.New("db error")
claims := auth.Claims{ID: "mf-123", ShareID: "share123"}
token, _ := auth.CreateExpiringPublicToken(time.Now().Add(time.Hour), claims)
w := makeRequest(token)
Expect(w.Code).To(Equal(http.StatusInternalServerError))
})
It("skips share check for tokens without shareID (backward compat)", func() {
claims := auth.Claims{ID: "mf-123"}
token, _ := auth.CreatePublicToken(claims)
w := makeRequest(token)
// Should get past share check, then fail on media file lookup (no mock data)
Expect(w.Code).To(Equal(http.StatusNotFound))
})
It("returns 400 for an invalid token", func() {
w := makeRequest("not-a-valid-token")
Expect(w.Code).To(Equal(http.StatusBadRequest))
})
})