mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-15 22:07:08 +02:00
* refactor(persistence): adopt generic deluan/rest repository API Pin deluan/rest to the refactor branch. REST-facing repository methods take a context and return typed values. Drop DataStore.Resource and ResourceRepository; the native API names typed repositories directly through a per-request adapter that later commits remove. * refactor(persistence): base repository helpers take a context * refactor(persistence): LibraryRepository takes a context per call * refactor(persistence): PropertyRepository takes a context per call * refactor(persistence): UserPropsRepository takes a context per call * refactor(persistence): TranscodingRepository takes a context per call * refactor(persistence): ShareRepository takes a context per call * refactor(persistence): PlayerRepository takes a context per call * refactor(persistence): RadioRepository takes a context per call * refactor(persistence): PlayQueueRepository takes a context per call * refactor(persistence): Tag and Genre repositories take a context per call * refactor(persistence): PluginRepository takes a context per call * refactor(persistence): Scrobble repositories take a context per call * refactor(persistence): FolderRepository takes a context per call * refactor(persistence): Artwork repositories take a context per call * refactor(persistence): UserRepository takes a context per call * refactor(persistence): ArtistRepository takes a context per call ReadAll no longer rewrites the shared sort mappings for the role filter; it works on a per-call copy. * test(persistence): assert artist role sort sanitization in ReadAll * refactor(persistence): AlbumRepository takes a context per call * test(persistence): pass the test context to album repository helpers * refactor(persistence): MediaFileRepository takes a context per call * refactor(persistence): Playlist repositories take a context per call * refactor(persistence): build all repositories once per store * refactor(core): REST repository wrappers are built once * refactor(persistence): repositories are stateless Remove the context field from the base repository and the per-request REST adapter. Enable the containedctx linter so no repository can hold a request context again. * chore(lint): skip containedctx in test files * refactor: share simplifications from the stateless repositories sweep Add deleteOwnedAll on sqlRepository and use it in player/share Delete to remove the duplicated bulk-delete loop; have Share.Repository() return model.ShareRepository so subsonic sharing.go drops its repeated type assertions. * chore(core): assert REST wrappers implement Persistable * chore: reformat imports * perf(persistence): build repositories on first use Each transaction store used to construct all 21 repositories up front, paying for filter and sort mapping setup the block never touched. Fields are now sync.OnceValue thunks, so a store only builds what it uses. * fix(persistence): clean plugin references per deleted user A bulk user delete that fails on a later id had already removed the earlier rows but skipped their plugin cleanup. Cleanup now runs right after each successful delete. * fix(core): unload disabled plugins even when a user delete fails A bulk delete can fail on a later id after earlier users were removed and their plugins auto-disabled. The wrapper returned before unloading, leaving those plugins running until the next successful delete or a restart. * chore(deps): pin deluan/rest to v1.0.1 Replaces the pseudo-version of the refactor branch with the tagged release. REST error messages now name the bare type (Artist, not model.Artist). * test: use the spec context instead of context.Background() Replace the context.Background()/context.TODO() calls this branch added to tests with the spec's ctx, GinkgoT().Context(), or t/b.Context(), so repository calls are bound to the running spec's lifetime. * test: declare the spec context once per Describe Set ctx from GinkgoT().Context() first in each top-level BeforeEach and reuse it, building user contexts on top of it instead of repeating inline calls.
435 lines
17 KiB
Go
435 lines
17 KiB
Go
package e2e
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"testing/fstest"
|
|
"time"
|
|
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/conf/configtest"
|
|
"github.com/navidrome/navidrome/core"
|
|
"github.com/navidrome/navidrome/core/artwork"
|
|
"github.com/navidrome/navidrome/core/auth"
|
|
"github.com/navidrome/navidrome/core/external"
|
|
"github.com/navidrome/navidrome/core/lyrics"
|
|
"github.com/navidrome/navidrome/core/metrics"
|
|
"github.com/navidrome/navidrome/core/playback"
|
|
"github.com/navidrome/navidrome/core/playlists"
|
|
"github.com/navidrome/navidrome/core/scrobbler"
|
|
"github.com/navidrome/navidrome/core/storage/storagetest"
|
|
"github.com/navidrome/navidrome/core/stream"
|
|
"github.com/navidrome/navidrome/db"
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/model/request"
|
|
"github.com/navidrome/navidrome/persistence"
|
|
"github.com/navidrome/navidrome/scanner"
|
|
"github.com/navidrome/navidrome/server/events"
|
|
"github.com/navidrome/navidrome/server/subsonic"
|
|
"github.com/navidrome/navidrome/server/subsonic/responses"
|
|
"github.com/navidrome/navidrome/tests"
|
|
"github.com/navidrome/navidrome/tests/harness"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
func TestSubsonicE2E(t *testing.T) {
|
|
tests.Init(t, false)
|
|
defer db.Close(t.Context())
|
|
log.SetLevel(log.LevelFatal)
|
|
RegisterFailHandler(Fail)
|
|
RunSpecs(t, "Subsonic API E2E Suite")
|
|
}
|
|
|
|
// Easy aliases for the storagetest package
|
|
type _t = map[string]any
|
|
|
|
var template = storagetest.Template
|
|
var track = storagetest.Track
|
|
var file = storagetest.File
|
|
|
|
// MusicBrainz ID constants for test data (valid UUID v4 values)
|
|
const (
|
|
mbidBeatlesArtist = "b10bbbfc-cf9e-42e0-be17-e2c3e1d2600d"
|
|
mbidAbbeyRoadAlbum = "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
|
|
mbidAbbeyRoadRelGroup = "d4c3b2a1-f6e5-4b7a-9d8c-1f0e3a2b5c4d"
|
|
mbidComeTogether = "11111111-1111-4111-a111-111111111111" // mbz_release_track_id
|
|
mbidComeTogetherRec = "22222222-2222-4222-a222-222222222222" // mbz_recording_id
|
|
mbidSomething = "33333333-3333-4333-a333-333333333333" // mbz_release_track_id
|
|
mbidSomethingRec = "44444444-4444-4444-a444-444444444444" // mbz_recording_id
|
|
)
|
|
|
|
// lyricFixture reads a public-domain lyric fixture (the same files the parser
|
|
// benchmarks use) so the e2e fixtures stay in sync with real-world content,
|
|
// including the word-level timing carried by the .elrc and .yaml variants.
|
|
func lyricFixture(name string) string {
|
|
// tests.Init chdirs to the project root, so reference fixtures from there.
|
|
data, err := os.ReadFile(filepath.Join("tests", "fixtures", "lyrics", name))
|
|
Expect(err).ToNot(HaveOccurred(), "reading lyric fixture %q", name)
|
|
return string(data)
|
|
}
|
|
|
|
// firstFixtureLine is the opening lyric line shared by every auld-lang-syne
|
|
// fixture; tests assert against it regardless of source format.
|
|
const firstFixtureLine = "Should auld acquaintance be forgot,"
|
|
|
|
// Shared test state
|
|
var (
|
|
ctx context.Context
|
|
ds *tests.MockDataStore
|
|
router *subsonic.Router
|
|
streamerSpy *harness.SpyStreamer
|
|
goldenDB *harness.DB
|
|
lib model.Library
|
|
|
|
// Admin user used for most tests
|
|
adminUser = model.User{
|
|
ID: "admin-1",
|
|
UserName: "admin",
|
|
Name: "Admin User",
|
|
IsAdmin: true,
|
|
}
|
|
|
|
// Regular (non-admin) user for permission tests
|
|
regularUser = model.User{
|
|
ID: "regular-1",
|
|
UserName: "regular",
|
|
Name: "Regular User",
|
|
IsAdmin: false,
|
|
}
|
|
)
|
|
|
|
// buildTestFS creates the full test filesystem matching the plan
|
|
func buildTestFS() storagetest.FakeFS {
|
|
abbeyRoad := template(_t{
|
|
"albumartist": "The Beatles",
|
|
"artist": "The Beatles",
|
|
"album": "Abbey Road",
|
|
"year": 1969,
|
|
"genre": "Rock",
|
|
"musicbrainz_artistid": mbidBeatlesArtist,
|
|
"musicbrainz_albumartistid": mbidBeatlesArtist,
|
|
"musicbrainz_albumid": mbidAbbeyRoadAlbum,
|
|
"musicbrainz_releasegroupid": mbidAbbeyRoadRelGroup,
|
|
})
|
|
help := template(_t{"albumartist": "The Beatles", "artist": "The Beatles", "album": "Help!", "year": 1965, "genre": "Rock"})
|
|
ledZepIV := template(_t{"albumartist": "Led Zeppelin", "artist": "Led Zeppelin", "album": "IV", "year": 1971, "genre": "Rock"})
|
|
kindOfBlue := template(_t{"albumartist": "Miles Davis", "artist": "Miles Davis", "album": "Kind of Blue", "year": 1959, "genre": "Jazz"})
|
|
popTrack := template(_t{"albumartist": "Various", "artist": "Various", "album": "Pop", "year": 2020, "genre": "Pop"})
|
|
cowboyBebop := template(_t{"albumartist": "シートベルツ", "artist": "シートベルツ", "album": "COWBOY BEBOP", "year": 1998, "genre": "Jazz"})
|
|
|
|
// Template for diverse-format transcode test tracks
|
|
tcBase := _t{"albumartist": "Test Artist", "artist": "Test Artist", "album": "Transcode Formats", "year": 2024, "genre": "Test"}
|
|
|
|
// Template for lyrics e2e fixture tracks — isolated under Lyrics/ to keep other suite counts stable
|
|
lyricsAlbum := template(_t{"albumartist": "Lyric Tester", "artist": "Lyric Tester", "album": "Lyrics", "year": 2024, "genre": "Test"})
|
|
|
|
return harness.CreateFS(fstest.MapFS{
|
|
// Rock / The Beatles / Abbey Road (with MBIDs)
|
|
// Note: "musicbrainz_trackid" is an alias for the musicbrainz_recordingid tag (populates MbzRecordingID),
|
|
// "musicbrainz_releasetrackid" is an alias for the musicbrainz_trackid tag (populates MbzReleaseTrackID).
|
|
"Rock/The Beatles/Abbey Road/01 - Come Together.mp3": abbeyRoad(track(1, "Come Together",
|
|
_t{"musicbrainz_releasetrackid": mbidComeTogether, "musicbrainz_trackid": mbidComeTogetherRec, "bpm": 120})),
|
|
"Rock/The Beatles/Abbey Road/02 - Something.mp3": abbeyRoad(track(2, "Something",
|
|
_t{"musicbrainz_releasetrackid": mbidSomething, "musicbrainz_trackid": mbidSomethingRec})),
|
|
// Rock / The Beatles / Help! (no MBIDs)
|
|
"Rock/The Beatles/Help!/01 - Help.mp3": help(track(1, "Help!")),
|
|
// Rock / Led Zeppelin / IV (no MBIDs)
|
|
"Rock/Led Zeppelin/IV/01 - Stairway To Heaven.mp3": ledZepIV(track(1, "Stairway To Heaven")),
|
|
// Jazz / Miles Davis / Kind of Blue (no MBIDs)
|
|
"Jazz/Miles Davis/Kind of Blue/01 - So What.mp3": kindOfBlue(track(1, "So What")),
|
|
// Pop (standalone track, no MBIDs)
|
|
"Pop/01 - Standalone Track.mp3": popTrack(track(1, "Standalone Track")),
|
|
// CJK / シートベルツ / COWBOY BEBOP (Japanese artist, for CJK search tests)
|
|
"CJK/シートベルツ/COWBOY BEBOP/01 - プラチナ・ジェット.mp3": cowboyBebop(track(1, "プラチナ・ジェット")),
|
|
|
|
// Diverse audio format tracks for transcode e2e tests
|
|
"Test/Transcode Formats/01 - TC FLAC Standard.flac": file(tcBase, _t{
|
|
"title": "TC FLAC Standard", "track": 1, "suffix": "flac",
|
|
"bitrate": 900, "samplerate": 44100, "bitdepth": 16, "channels": 2, "duration": int64(240),
|
|
}),
|
|
"Test/Transcode Formats/02 - TC FLAC HiRes.flac": file(tcBase, _t{
|
|
"title": "TC FLAC HiRes", "track": 2, "suffix": "flac",
|
|
"bitrate": 3000, "samplerate": 96000, "bitdepth": 24, "channels": 2, "duration": int64(180),
|
|
}),
|
|
"Test/Transcode Formats/03 - TC ALAC Track.m4a": file(tcBase, _t{
|
|
"title": "TC ALAC Track", "track": 3, "suffix": "m4a",
|
|
"bitrate": 900, "samplerate": 44100, "bitdepth": 16, "channels": 2, "duration": int64(200),
|
|
}),
|
|
"Test/Transcode Formats/04 - TC DSD Track.dsf": file(tcBase, _t{
|
|
"title": "TC DSD Track", "track": 4, "suffix": "dsf",
|
|
"bitrate": 5645, "samplerate": 2822400, "bitdepth": 1, "channels": 2, "duration": int64(300),
|
|
}),
|
|
"Test/Transcode Formats/05 - TC Opus Track.opus": file(tcBase, _t{
|
|
"title": "TC Opus Track", "track": 5, "suffix": "opus",
|
|
"bitrate": 128, "samplerate": 48000, "bitdepth": 0, "channels": 2, "duration": int64(210),
|
|
}),
|
|
"Test/Transcode Formats/06 - TC MKA Opus.mka": file(tcBase, _t{
|
|
"title": "TC MKA Opus", "track": 6, "suffix": "mka", "codec": "opus",
|
|
"bitrate": 128, "samplerate": 48000, "bitdepth": 0, "channels": 2, "duration": int64(220),
|
|
}),
|
|
"Test/Transcode Formats/07 - TC FLAC Multichannel.flac": file(tcBase, _t{
|
|
"title": "TC FLAC Multichannel", "track": 7, "suffix": "flac",
|
|
"bitrate": 4500, "samplerate": 48000, "bitdepth": 24, "channels": 6, "duration": int64(180),
|
|
}),
|
|
|
|
// Lyrics fixtures (isolated under Lyrics/ to keep other suite counts stable).
|
|
// Content comes from tests/fixtures/lyrics (the same public-domain files the
|
|
// parser benchmarks use); the .elrc and .yaml variants carry word-level
|
|
// timing, which drives the v1 (line-level) vs v2 (enhanced/word-level) tests.
|
|
//
|
|
// Embedded — lyrics delivered via the "lyrics" tag, parsed at scan time.
|
|
// "Enhanced LRC" embeds ELRC (word-level) content; the title is kept generic
|
|
// since ELRC is still valid LRC.
|
|
"Lyrics/Embedded/01 - Embedded Enhanced LRC.mp3": lyricsAlbum(track(1, "Embedded Enhanced LRC",
|
|
_t{"lyrics": lyricFixture("auld-lang-syne.elrc")})),
|
|
"Lyrics/Embedded/02 - Embedded Plain.mp3": lyricsAlbum(track(2, "Embedded Plain",
|
|
_t{"lyrics": lyricFixture("auld-lang-syne.txt")})),
|
|
"Lyrics/Embedded/03 - Embedded TTML.mp3": lyricsAlbum(track(3, "Embedded TTML",
|
|
_t{"lyrics": lyricFixture("auld-lang-syne.ttml")})),
|
|
|
|
// Sidecar — raw lyric text files read from the library FS at request time via fromExternalFile.
|
|
// The scanner skips non-audio extensions (.lrc, .srt, .yaml), so placing them as raw MapFile
|
|
// entries is safe: they are visible to the fake FS but invisible to the scanner.
|
|
"Lyrics/Sidecar/01 - Sidecar LRC.mp3": lyricsAlbum(track(1, "Sidecar LRC")),
|
|
"Lyrics/Sidecar/01 - Sidecar LRC.lrc": &fstest.MapFile{Data: []byte(lyricFixture("auld-lang-syne.lrc")), ModTime: time.Now()},
|
|
"Lyrics/Sidecar/02 - Sidecar SRT.mp3": lyricsAlbum(track(2, "Sidecar SRT")),
|
|
"Lyrics/Sidecar/02 - Sidecar SRT.srt": &fstest.MapFile{Data: []byte(lyricFixture("auld-lang-syne.srt")), ModTime: time.Now()},
|
|
"Lyrics/Sidecar/03 - Sidecar YAML.mp3": lyricsAlbum(track(3, "Sidecar YAML")),
|
|
"Lyrics/Sidecar/03 - Sidecar YAML.yaml": &fstest.MapFile{Data: []byte(lyricFixture("auld-lang-syne.yaml")), ModTime: time.Now()},
|
|
|
|
// _empty folder (directory with no audio)
|
|
"_empty/.keep": &fstest.MapFile{Data: []byte{}, ModTime: time.Now()},
|
|
})
|
|
}
|
|
|
|
// createUser creates a user in the database with the given properties, assigns them to the test
|
|
// library, and returns the fully-loaded user (with Libraries populated).
|
|
func createUser(id, username, name string, isAdmin bool) model.User {
|
|
user := model.User{
|
|
ID: id,
|
|
UserName: username,
|
|
Name: name,
|
|
IsAdmin: isAdmin,
|
|
NewPassword: "password",
|
|
}
|
|
Expect(ds.User().Put(ctx, &user)).To(Succeed())
|
|
Expect(ds.User().SetUserLibraries(ctx, user.ID, []int{lib.ID})).To(Succeed())
|
|
|
|
loadedUser, err := ds.User().FindByUsername(ctx, user.UserName)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
user.Libraries = loadedUser.Libraries
|
|
return user
|
|
}
|
|
|
|
// doReq makes a full HTTP round-trip through the router and returns the parsed Subsonic response.
|
|
func doReq(endpoint string, params ...string) *responses.Subsonic {
|
|
return doReqWithUser(adminUser, endpoint, params...)
|
|
}
|
|
|
|
// doReqWithUser makes a full HTTP round-trip for the given user and returns the parsed Subsonic response.
|
|
func doReqWithUser(user model.User, endpoint string, params ...string) *responses.Subsonic {
|
|
w := httptest.NewRecorder()
|
|
r := buildReq(user, endpoint, params...)
|
|
router.ServeHTTP(w, r)
|
|
return parseJSONResponse(w)
|
|
}
|
|
|
|
// doRawReq returns the raw ResponseRecorder for endpoints that write binary data (stream, download, getCoverArt).
|
|
func doRawReq(endpoint string, params ...string) *httptest.ResponseRecorder {
|
|
return doRawReqWithUser(adminUser, endpoint, params...)
|
|
}
|
|
|
|
// doRawReqWithUser returns the raw ResponseRecorder for the given user.
|
|
func doRawReqWithUser(user model.User, endpoint string, params ...string) *httptest.ResponseRecorder {
|
|
w := httptest.NewRecorder()
|
|
r := buildReq(user, endpoint, params...)
|
|
router.ServeHTTP(w, r)
|
|
return w
|
|
}
|
|
|
|
// buildReq creates a GET request with Subsonic auth params (u, p, v, c, f=json).
|
|
func buildReq(user model.User, endpoint string, params ...string) *http.Request {
|
|
if len(params)%2 != 0 {
|
|
panic("buildReq: odd number of parameters")
|
|
}
|
|
q := url.Values{}
|
|
q.Add("u", user.UserName)
|
|
q.Add("p", "password")
|
|
q.Add("v", "1.16.1")
|
|
q.Add("c", "test-client")
|
|
q.Add("f", "json")
|
|
for i := 0; i < len(params); i += 2 {
|
|
q.Add(params[i], params[i+1])
|
|
}
|
|
return httptest.NewRequest("GET", "/"+endpoint+"?"+q.Encode(), nil)
|
|
}
|
|
|
|
// buildPostReq creates a POST request with a JSON body and Subsonic auth params in the query string.
|
|
func buildPostReq(user model.User, endpoint string, body string, params ...string) *http.Request {
|
|
getReq := buildReq(user, endpoint, params...)
|
|
r := httptest.NewRequest("POST", getReq.URL.RequestURI(), bytes.NewReader([]byte(body)))
|
|
r.Header.Set("Content-Type", "application/json")
|
|
return r
|
|
}
|
|
|
|
// doPostReq makes a POST round-trip as admin and returns the parsed Subsonic response.
|
|
func doPostReq(endpoint string, body string, params ...string) *responses.Subsonic {
|
|
w := httptest.NewRecorder()
|
|
r := buildPostReq(adminUser, endpoint, body, params...)
|
|
router.ServeHTTP(w, r)
|
|
return parseJSONResponse(w)
|
|
}
|
|
|
|
// doRawPostReq makes a POST round-trip as admin and returns the raw recorder.
|
|
func doRawPostReq(endpoint string, body string, params ...string) *httptest.ResponseRecorder {
|
|
w := httptest.NewRecorder()
|
|
r := buildPostReq(adminUser, endpoint, body, params...)
|
|
router.ServeHTTP(w, r)
|
|
return w
|
|
}
|
|
|
|
// parseJSONResponse parses the JSON response body into a Subsonic response struct.
|
|
func parseJSONResponse(w *httptest.ResponseRecorder) *responses.Subsonic {
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
var wrapper responses.JsonWrapper
|
|
Expect(json.Unmarshal(w.Body.Bytes(), &wrapper)).To(Succeed())
|
|
return &wrapper.Subsonic
|
|
}
|
|
|
|
// --- Noop stub implementations for Router dependencies ---
|
|
|
|
// noopArtwork implements artwork.Artwork
|
|
type noopArtwork struct{}
|
|
|
|
func (n noopArtwork) Get(context.Context, model.ArtworkID, int, bool) (*artwork.Image, error) {
|
|
return nil, model.ErrNotFound
|
|
}
|
|
|
|
func (n noopArtwork) GetOrPlaceholder(_ context.Context, _ string, _ int, _ bool) (*artwork.Image, error) {
|
|
return &artwork.Image{ReadCloser: io.NopCloser(io.LimitReader(nil, 0))}, nil
|
|
}
|
|
|
|
// noopArchiver implements core.Archiver
|
|
type noopArchiver struct{}
|
|
|
|
func (n noopArchiver) ZipAlbum(context.Context, string, string, int, io.Writer) error {
|
|
return model.ErrNotFound
|
|
}
|
|
|
|
func (n noopArchiver) ZipArtist(context.Context, string, string, int, io.Writer) error {
|
|
return model.ErrNotFound
|
|
}
|
|
|
|
func (n noopArchiver) ZipShare(context.Context, *model.Share, io.Writer) error {
|
|
return model.ErrNotFound
|
|
}
|
|
|
|
func (n noopArchiver) ZipPlaylist(context.Context, string, string, int, io.Writer) error {
|
|
return model.ErrNotFound
|
|
}
|
|
|
|
// noopProvider implements external.Provider
|
|
type noopProvider struct{}
|
|
|
|
func (n noopProvider) UpdateAlbumInfo(_ context.Context, _ string) (*model.Album, error) {
|
|
return &model.Album{}, nil
|
|
}
|
|
|
|
func (n noopProvider) UpdateArtistInfo(_ context.Context, _ string, _ int, _ bool) (*model.Artist, error) {
|
|
return &model.Artist{}, nil
|
|
}
|
|
|
|
func (n noopProvider) SimilarSongs(context.Context, string, int) (model.MediaFiles, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (n noopProvider) TopSongs(context.Context, string, string, int) (model.MediaFiles, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (n noopProvider) RefreshInfo(context.Context, model.Kind, string) error {
|
|
return nil
|
|
}
|
|
|
|
// Compile-time interface checks
|
|
var (
|
|
_ artwork.Artwork = noopArtwork{}
|
|
_ core.Archiver = noopArchiver{}
|
|
_ external.Provider = noopProvider{}
|
|
)
|
|
|
|
var _ = BeforeSuite(func() {
|
|
ctx = request.WithUser(GinkgoT().Context(), adminUser)
|
|
|
|
conf.Server.MusicFolder = "fake:///music"
|
|
conf.Server.LyricsPriority = "embedded,.lrc,.srt,.yaml"
|
|
conf.Server.DevExternalScanner = false
|
|
|
|
buildTestFS()
|
|
goldenDB = harness.SetupDB(ctx, &adminUser, ®ularUser)
|
|
lib = goldenDB.Library
|
|
ctx = request.WithUser(GinkgoT().Context(), adminUser)
|
|
})
|
|
|
|
// Close the database before the suite's TempDir cleanup runs. Required on
|
|
// Windows where open SQLite handles hold file locks that block temp-dir
|
|
// removal; harmless on other OSes.
|
|
var _ = AfterSuite(func() {
|
|
db.Close(ctx)
|
|
})
|
|
|
|
// setupTestDB restores the database from the golden snapshot and creates the
|
|
// Subsonic Router. Call this from BeforeEach/BeforeAll in each test container.
|
|
func setupTestDB() {
|
|
ctx = request.WithUser(GinkgoT().Context(), adminUser)
|
|
|
|
DeferCleanup(configtest.SetupConfig())
|
|
DeferCleanup(func() {
|
|
// Wait for any background scan (e.g. from startScan endpoint) to finish
|
|
// before config cleanup runs, to avoid a data race on conf.Server.
|
|
Eventually(scanner.IsScanning).Should(BeFalse())
|
|
})
|
|
conf.Server.MusicFolder = "fake:///music"
|
|
conf.Server.DevExternalScanner = false
|
|
conf.Server.DevEnableMediaFileProbe = false
|
|
|
|
// Restore DB to golden state (no scan needed)
|
|
goldenDB.Restore()
|
|
|
|
ds = &tests.MockDataStore{RealDS: persistence.New(db.Db())}
|
|
auth.Init(ds)
|
|
|
|
// Create the Subsonic Router with real DS, streamer spy, and real Decider
|
|
streamerSpy = &harness.SpyStreamer{}
|
|
decider := stream.NewTranscodeDecider(ds, harness.NoopFFmpeg{})
|
|
s := scanner.New(ctx, ds, events.NoopBroker(),
|
|
playlists.NewPlaylists(ds, artwork.NewUploader(ds)), metrics.NewNoopInstance())
|
|
router = subsonic.New(
|
|
ds,
|
|
noopArtwork{},
|
|
streamerSpy,
|
|
noopArchiver{},
|
|
core.NewPlayers(ds),
|
|
noopProvider{},
|
|
s,
|
|
events.NoopBroker(),
|
|
playlists.NewPlaylists(ds, artwork.NewUploader(ds)),
|
|
scrobbler.NewPlayTracker(ds, events.NoopBroker(), nil),
|
|
core.NewShare(ds),
|
|
playback.PlaybackServer(nil),
|
|
metrics.NewNoopInstance(),
|
|
lyrics.NewLyrics(ds, nil),
|
|
decider,
|
|
nil,
|
|
)
|
|
}
|