mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
test(e2e): add NewTestStream function and implement spyStreamer for testing
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
a1a55141f6
commit
a46cd491fb
@ -6,6 +6,7 @@ import (
|
||||
"io"
|
||||
"mime"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@ -142,6 +143,17 @@ func (s *Stream) EstimatedContentLength() int {
|
||||
return int(s.mf.Duration * float32(s.bitRate) / 8 * 1024)
|
||||
}
|
||||
|
||||
// NewTestStream creates a Stream for testing purposes.
|
||||
func NewTestStream(mf *model.MediaFile, format string, bitRate int) *Stream {
|
||||
return &Stream{
|
||||
ctx: context.Background(),
|
||||
mf: mf,
|
||||
format: format,
|
||||
bitRate: bitRate,
|
||||
ReadCloser: io.NopCloser(strings.NewReader("")),
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
onceTranscodingCache sync.Once
|
||||
instanceTranscodingCache TranscodingCache
|
||||
|
||||
@ -3,6 +3,7 @@ package e2e
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@ -19,6 +20,7 @@ import (
|
||||
"github.com/navidrome/navidrome/core/artwork"
|
||||
"github.com/navidrome/navidrome/core/auth"
|
||||
"github.com/navidrome/navidrome/core/external"
|
||||
"github.com/navidrome/navidrome/core/ffmpeg"
|
||||
"github.com/navidrome/navidrome/core/lyrics"
|
||||
"github.com/navidrome/navidrome/core/metrics"
|
||||
"github.com/navidrome/navidrome/core/playback"
|
||||
@ -70,6 +72,7 @@ var (
|
||||
ctx context.Context
|
||||
ds *tests.MockDataStore
|
||||
router *subsonic.Router
|
||||
spy *spyStreamer
|
||||
lib model.Library
|
||||
|
||||
// Snapshot paths for fast DB restore
|
||||
@ -225,36 +228,50 @@ func (n noopArtwork) GetOrPlaceholder(_ context.Context, _ string, _ int, _ bool
|
||||
return io.NopCloser(io.LimitReader(nil, 0)), time.Time{}, nil
|
||||
}
|
||||
|
||||
// noopStreamer implements transcode.MediaStreamer
|
||||
type noopStreamer struct{}
|
||||
// spyStreamer captures the StreamRequest passed to DoStream for test assertions,
|
||||
// then returns a minimal fake Stream so the handler completes without error.
|
||||
type spyStreamer struct {
|
||||
LastRequest transcode.StreamRequest
|
||||
LastMediaFile *model.MediaFile
|
||||
}
|
||||
|
||||
func (n noopStreamer) NewStream(context.Context, transcode.StreamRequest) (*transcode.Stream, error) {
|
||||
func (s *spyStreamer) NewStream(ctx context.Context, req transcode.StreamRequest) (*transcode.Stream, error) {
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
|
||||
func (n noopStreamer) DoStream(context.Context, *model.MediaFile, transcode.StreamRequest) (*transcode.Stream, error) {
|
||||
return nil, model.ErrNotFound
|
||||
func (s *spyStreamer) DoStream(_ context.Context, mf *model.MediaFile, req transcode.StreamRequest) (*transcode.Stream, error) {
|
||||
s.LastRequest = req
|
||||
s.LastMediaFile = mf
|
||||
format := req.Format
|
||||
if format == "" || format == "raw" {
|
||||
format = mf.Suffix
|
||||
}
|
||||
return transcode.NewTestStream(mf, format, req.BitRate), nil
|
||||
}
|
||||
|
||||
// noopDecider implements transcode.Decider
|
||||
type noopDecider struct{}
|
||||
// noopFFmpeg implements ffmpeg.FFmpeg with no-op methods.
|
||||
type noopFFmpeg struct{}
|
||||
|
||||
func (n noopDecider) MakeDecision(context.Context, *model.MediaFile, *transcode.ClientInfo, transcode.DecisionOptions) (*transcode.Decision, error) {
|
||||
return nil, nil
|
||||
func (n noopFFmpeg) Transcode(context.Context, ffmpeg.TranscodeOptions) (io.ReadCloser, error) {
|
||||
return nil, errors.New("noop ffmpeg: transcode not supported")
|
||||
}
|
||||
|
||||
func (n noopDecider) ResolveRequest(context.Context, *model.MediaFile, string, int, int) transcode.StreamRequest {
|
||||
return transcode.StreamRequest{Format: "raw"}
|
||||
func (n noopFFmpeg) ExtractImage(context.Context, string) (io.ReadCloser, error) {
|
||||
return nil, errors.New("noop ffmpeg: extract image not supported")
|
||||
}
|
||||
|
||||
func (n noopDecider) CreateTranscodeParams(*transcode.Decision) (string, error) {
|
||||
func (n noopFFmpeg) Probe(context.Context, []string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (n noopDecider) ResolveRequestFromToken(context.Context, string, string, int) (transcode.StreamRequest, *model.MediaFile, error) {
|
||||
return transcode.StreamRequest{}, nil, nil
|
||||
func (n noopFFmpeg) ProbeAudioStream(context.Context, string) (*ffmpeg.AudioProbeResult, error) {
|
||||
return nil, errors.New("noop ffmpeg: probe not supported")
|
||||
}
|
||||
|
||||
func (n noopFFmpeg) CmdPath() (string, error) { return "", nil }
|
||||
func (n noopFFmpeg) IsAvailable() bool { return false }
|
||||
func (n noopFFmpeg) Version() string { return "noop" }
|
||||
|
||||
// noopArchiver implements core.Archiver
|
||||
type noopArchiver struct{}
|
||||
|
||||
@ -319,11 +336,11 @@ func (n noopPlayTracker) Submit(context.Context, []scrobbler.Submission) error {
|
||||
// Compile-time interface checks
|
||||
var (
|
||||
_ artwork.Artwork = noopArtwork{}
|
||||
_ transcode.MediaStreamer = noopStreamer{}
|
||||
_ transcode.MediaStreamer = &spyStreamer{}
|
||||
_ core.Archiver = noopArchiver{}
|
||||
_ external.Provider = noopProvider{}
|
||||
_ scrobbler.PlayTracker = noopPlayTracker{}
|
||||
_ transcode.Decider = noopDecider{}
|
||||
_ ffmpeg.FFmpeg = noopFFmpeg{}
|
||||
)
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
@ -401,13 +418,15 @@ func setupTestDB() {
|
||||
ds = &tests.MockDataStore{RealDS: persistence.New(db.Db())}
|
||||
auth.Init(ds)
|
||||
|
||||
// Create the Subsonic Router with real DS + noop stubs
|
||||
// Create the Subsonic Router with real DS, spy streamer, and real Decider
|
||||
spy = &spyStreamer{}
|
||||
decider := transcode.NewDecider(ds, noopFFmpeg{})
|
||||
s := scanner.New(ctx, ds, artwork.NoopCacheWarmer(), events.NoopBroker(),
|
||||
playlists.NewPlaylists(ds), metrics.NewNoopInstance())
|
||||
router = subsonic.New(
|
||||
ds,
|
||||
noopArtwork{},
|
||||
noopStreamer{},
|
||||
spy,
|
||||
noopArchiver{},
|
||||
core.NewPlayers(ds),
|
||||
noopProvider{},
|
||||
@ -419,7 +438,7 @@ func setupTestDB() {
|
||||
playback.PlaybackServer(nil),
|
||||
metrics.NewNoopInstance(),
|
||||
lyrics.NewLyrics(nil),
|
||||
noopDecider{},
|
||||
decider,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,9 @@ package e2e
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/conf/configtest"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
@ -14,21 +17,142 @@ var _ = Describe("Media Retrieval Endpoints", Ordered, func() {
|
||||
})
|
||||
|
||||
Describe("Stream", func() {
|
||||
var trackID string
|
||||
|
||||
BeforeAll(func() {
|
||||
// All test tracks are mp3 at 320kbps
|
||||
songs, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{Max: 1, Sort: "title"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(songs).ToNot(BeEmpty())
|
||||
trackID = songs[0].ID
|
||||
})
|
||||
|
||||
It("returns error when id parameter is missing", func() {
|
||||
resp := doReq("stream")
|
||||
|
||||
Expect(resp.Status).To(Equal(responses.StatusFailed))
|
||||
Expect(resp.Error).ToNot(BeNil())
|
||||
})
|
||||
|
||||
It("streams raw when no format or bitrate specified", func() {
|
||||
w := doRawReq("stream", "id", trackID)
|
||||
|
||||
Expect(w.Code).To(Equal(http.StatusOK))
|
||||
Expect(spy.LastRequest.Format).To(Equal("raw"))
|
||||
})
|
||||
|
||||
It("streams raw when format=raw", func() {
|
||||
w := doRawReq("stream", "id", trackID, "format", "raw")
|
||||
|
||||
Expect(w.Code).To(Equal(http.StatusOK))
|
||||
Expect(spy.LastRequest.Format).To(Equal("raw"))
|
||||
})
|
||||
|
||||
It("transcodes to different format with bitrate", func() {
|
||||
w := doRawReq("stream", "id", trackID, "format", "opus", "maxBitRate", "128")
|
||||
|
||||
Expect(w.Code).To(Equal(http.StatusOK))
|
||||
Expect(spy.LastRequest.Format).To(Equal("opus"))
|
||||
Expect(spy.LastRequest.BitRate).To(Equal(128))
|
||||
})
|
||||
|
||||
It("downsamples when only maxBitRate is specified (lower than source)", func() {
|
||||
DeferCleanup(configtest.SetupConfig())
|
||||
conf.Server.DefaultDownsamplingFormat = "opus"
|
||||
|
||||
w := doRawReq("stream", "id", trackID, "maxBitRate", "128")
|
||||
|
||||
Expect(w.Code).To(Equal(http.StatusOK))
|
||||
Expect(spy.LastRequest.Format).To(Equal("opus"))
|
||||
Expect(spy.LastRequest.BitRate).To(Equal(128))
|
||||
})
|
||||
|
||||
It("streams raw when maxBitRate is higher than source", func() {
|
||||
w := doRawReq("stream", "id", trackID, "maxBitRate", "999")
|
||||
|
||||
Expect(w.Code).To(Equal(http.StatusOK))
|
||||
Expect(spy.LastRequest.Format).To(Equal("raw"))
|
||||
})
|
||||
|
||||
It("streams raw when format matches source and no bitrate reduction", func() {
|
||||
w := doRawReq("stream", "id", trackID, "format", "mp3", "maxBitRate", "320")
|
||||
|
||||
Expect(w.Code).To(Equal(http.StatusOK))
|
||||
Expect(spy.LastRequest.Format).To(Equal("raw"))
|
||||
})
|
||||
|
||||
It("transcodes when same format but lower bitrate", func() {
|
||||
w := doRawReq("stream", "id", trackID, "format", "mp3", "maxBitRate", "128")
|
||||
|
||||
Expect(w.Code).To(Equal(http.StatusOK))
|
||||
Expect(spy.LastRequest.Format).To(Equal("mp3"))
|
||||
Expect(spy.LastRequest.BitRate).To(Equal(128))
|
||||
})
|
||||
|
||||
It("falls back to raw for unknown format", func() {
|
||||
w := doRawReq("stream", "id", trackID, "format", "xyz")
|
||||
|
||||
Expect(w.Code).To(Equal(http.StatusOK))
|
||||
Expect(spy.LastRequest.Format).To(Equal("raw"))
|
||||
})
|
||||
|
||||
It("passes timeOffset through", func() {
|
||||
w := doRawReq("stream", "id", trackID, "format", "opus", "maxBitRate", "128", "timeOffset", "30")
|
||||
|
||||
Expect(w.Code).To(Equal(http.StatusOK))
|
||||
Expect(spy.LastRequest.Format).To(Equal("opus"))
|
||||
Expect(spy.LastRequest.Offset).To(Equal(30))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Download", func() {
|
||||
var trackID string
|
||||
|
||||
BeforeAll(func() {
|
||||
// All test tracks are mp3 at 320kbps
|
||||
songs, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{Max: 1, Sort: "title"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(songs).ToNot(BeEmpty())
|
||||
trackID = songs[0].ID
|
||||
})
|
||||
|
||||
It("returns error when id parameter is missing", func() {
|
||||
resp := doReq("download")
|
||||
|
||||
Expect(resp.Status).To(Equal(responses.StatusFailed))
|
||||
Expect(resp.Error).ToNot(BeNil())
|
||||
})
|
||||
|
||||
It("downloads raw when no format specified and AutoTranscodeDownload is false", func() {
|
||||
DeferCleanup(configtest.SetupConfig())
|
||||
conf.Server.EnableDownloads = true
|
||||
conf.Server.AutoTranscodeDownload = false
|
||||
|
||||
w := doRawReq("download", "id", trackID)
|
||||
|
||||
Expect(w.Code).To(Equal(http.StatusOK))
|
||||
Expect(spy.LastRequest.Format).To(Equal("raw"))
|
||||
})
|
||||
|
||||
It("downloads with explicit format and bitrate", func() {
|
||||
DeferCleanup(configtest.SetupConfig())
|
||||
conf.Server.EnableDownloads = true
|
||||
|
||||
w := doRawReq("download", "id", trackID, "format", "opus", "bitrate", "128")
|
||||
|
||||
Expect(w.Code).To(Equal(http.StatusOK))
|
||||
Expect(spy.LastRequest.Format).To(Equal("opus"))
|
||||
Expect(spy.LastRequest.BitRate).To(Equal(128))
|
||||
})
|
||||
|
||||
It("returns error when downloads are disabled", func() {
|
||||
DeferCleanup(configtest.SetupConfig())
|
||||
conf.Server.EnableDownloads = false
|
||||
|
||||
resp := doReq("download", "id", trackID)
|
||||
|
||||
Expect(resp.Status).To(Equal(responses.StatusFailed))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetCoverArt", func() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user