quintodrome/server/e2e/e2e_suite_test.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

390 lines
13 KiB
Go
Raw Normal View History

test(subsonic): add comprehensive e2e test suite for Subsonic API (#5003) * test(e2e): add comprehensive tests for Subsonic API endpoints Signed-off-by: Deluan <deluan@navidrome.org> * fix(e2e): improve database handling and snapshot restoration in tests Signed-off-by: Deluan <deluan@navidrome.org> * test(e2e): add tests for album sharing and user isolation scenarios Signed-off-by: Deluan <deluan@navidrome.org> * test(e2e): add tests for multi-library support and user access control Signed-off-by: Deluan <deluan@navidrome.org> * test(e2e): tests are fast, no need to skip on -short Signed-off-by: Deluan <deluan@navidrome.org> * address gemini comments Signed-off-by: Deluan <deluan@navidrome.org> * fix(tests): prevent MockDataStore from caching repos with stale context When RealDS is set, MockDataStore previously cached repository instances on first access, binding them to the initial caller's context. This meant repos created with an admin context would skip library filtering for all subsequent non-admin calls, silently masking access control bugs. Changed MockDataStore to delegate to RealDS on every call without caching, so each caller gets a fresh repo with the correct context. Removed the pre-warm calls in e2e setupTestDB that were working around the old caching behavior. * test(e2e): route subsonic tests through full HTTP middleware stack Replace direct router method calls with full HTTP round-trips via router.ServeHTTP(w, r) across all 15 e2e test files. Tests now exercise the complete chi middleware chain including postFormToQueryParams, checkRequiredParameters, authenticate, UpdateLastAccessMiddleware, getPlayer, and sendResponse/sendError serialization. New helpers (doReq, doReqWithUser, doRawReq, buildReq, parseJSONResponse) use plaintext password auth and JSON response format. Old helpers that injected context directly (newReq, newReqWithUser, newRawReq) are removed. Sharing tests now set conf.Server.EnableSharing before router creation to ensure sharing routes are registered. --------- Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-09 04:24:37 -09:00
package e2e
import (
"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/metrics"
"github.com/navidrome/navidrome/core/playback"
"github.com/navidrome/navidrome/core/scrobbler"
"github.com/navidrome/navidrome/core/storage/storagetest"
"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/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
// Shared test state
var (
ctx context.Context
ds *tests.MockDataStore
router *subsonic.Router
lib model.Library
// Snapshot paths for fast DB restore
dbFilePath string
snapshotPath string
// Admin user used for most tests
adminUser = model.User{
ID: "admin-1",
UserName: "admin",
Name: "Admin User",
IsAdmin: true,
}
)
func createFS(files fstest.MapFS) storagetest.FakeFS {
fs := storagetest.FakeFS{}
fs.SetFiles(files)
storagetest.Register("fake", &fs)
return fs
}
// 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"})
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"})
return createFS(fstest.MapFS{
// Rock / The Beatles / Abbey Road
"Rock/The Beatles/Abbey Road/01 - Come Together.mp3": abbeyRoad(track(1, "Come Together")),
"Rock/The Beatles/Abbey Road/02 - Something.mp3": abbeyRoad(track(2, "Something")),
// Rock / The Beatles / Help!
"Rock/The Beatles/Help!/01 - Help.mp3": help(track(1, "Help!")),
// Rock / Led Zeppelin / IV
"Rock/Led Zeppelin/IV/01 - Stairway To Heaven.mp3": ledZepIV(track(1, "Stairway To Heaven")),
// Jazz / Miles Davis / Kind of Blue
"Jazz/Miles Davis/Kind of Blue/01 - So What.mp3": kindOfBlue(track(1, "So What")),
// Pop (standalone track)
"Pop/01 - Standalone Track.mp3": popTrack(track(1, "Standalone Track")),
// _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(ctx).Put(&user)).To(Succeed())
Expect(ds.User(ctx).SetUserLibraries(user.ID, []int{lib.ID})).To(Succeed())
loadedUser, err := ds.User(ctx).FindByUsername(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)
}
// 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) (io.ReadCloser, time.Time, error) {
return nil, time.Time{}, model.ErrNotFound
}
func (n noopArtwork) GetOrPlaceholder(_ context.Context, _ string, _ int, _ bool) (io.ReadCloser, time.Time, error) {
return io.NopCloser(io.LimitReader(nil, 0)), time.Time{}, nil
}
// noopStreamer implements core.MediaStreamer
type noopStreamer struct{}
func (n noopStreamer) NewStream(context.Context, string, string, int, int) (*core.Stream, error) {
return nil, model.ErrNotFound
}
func (n noopStreamer) DoStream(context.Context, *model.MediaFile, string, int, int) (*core.Stream, error) {
return nil, model.ErrNotFound
}
// 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, string, 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, int) (model.MediaFiles, error) {
return nil, nil
}
func (n noopProvider) ArtistImage(context.Context, string) (*url.URL, error) {
return nil, model.ErrNotFound
}
func (n noopProvider) AlbumImage(context.Context, string) (*url.URL, error) {
return nil, model.ErrNotFound
}
// noopPlayTracker implements scrobbler.PlayTracker
type noopPlayTracker struct{}
func (n noopPlayTracker) NowPlaying(context.Context, string, string, string, int) error {
return nil
}
func (n noopPlayTracker) GetNowPlaying(context.Context) ([]scrobbler.NowPlayingInfo, error) {
return nil, nil
}
func (n noopPlayTracker) Submit(context.Context, []scrobbler.Submission) error {
return nil
}
// Compile-time interface checks
var (
_ artwork.Artwork = noopArtwork{}
_ core.MediaStreamer = noopStreamer{}
_ core.Archiver = noopArchiver{}
_ external.Provider = noopProvider{}
_ scrobbler.PlayTracker = noopPlayTracker{}
)
var _ = BeforeSuite(func() {
ctx = request.WithUser(GinkgoT().Context(), adminUser)
tmpDir := GinkgoT().TempDir()
dbFilePath = filepath.Join(tmpDir, "test-e2e.db")
snapshotPath = filepath.Join(tmpDir, "test-e2e.db.snapshot")
conf.Server.DbPath = dbFilePath + "?_journal_mode=WAL"
db.Db().SetMaxOpenConns(1)
// Initial setup: schema, user, library, and full scan (runs once for the entire suite)
conf.Server.MusicFolder = "fake:///music"
conf.Server.DevExternalScanner = false
db.Init(ctx)
initDS := &tests.MockDataStore{RealDS: persistence.New(db.Db())}
auth.Init(initDS)
adminUserWithPass := adminUser
adminUserWithPass.NewPassword = "password"
Expect(initDS.User(ctx).Put(&adminUserWithPass)).To(Succeed())
lib = model.Library{ID: 1, Name: "Music Library", Path: "fake:///music"}
Expect(initDS.Library(ctx).Put(&lib)).To(Succeed())
Expect(initDS.User(ctx).SetUserLibraries(adminUser.ID, []int{lib.ID})).To(Succeed())
loadedUser, err := initDS.User(ctx).FindByUsername(adminUser.UserName)
Expect(err).ToNot(HaveOccurred())
adminUser.Libraries = loadedUser.Libraries
ctx = request.WithUser(GinkgoT().Context(), adminUser)
buildTestFS()
s := scanner.New(ctx, initDS, artwork.NoopCacheWarmer(), events.NoopBroker(),
core.NewPlaylists(initDS), metrics.NewNoopInstance())
_, err = s.ScanAll(ctx, true)
Expect(err).ToNot(HaveOccurred())
// Checkpoint WAL and snapshot the golden DB state
_, err = db.Db().Exec("PRAGMA wal_checkpoint(TRUNCATE)")
Expect(err).ToNot(HaveOccurred())
data, err := os.ReadFile(dbFilePath)
Expect(err).ToNot(HaveOccurred())
Expect(os.WriteFile(snapshotPath, data, 0600)).To(Succeed())
})
// 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())
})
test(subsonic): add comprehensive e2e test suite for Subsonic API (#5003) * test(e2e): add comprehensive tests for Subsonic API endpoints Signed-off-by: Deluan <deluan@navidrome.org> * fix(e2e): improve database handling and snapshot restoration in tests Signed-off-by: Deluan <deluan@navidrome.org> * test(e2e): add tests for album sharing and user isolation scenarios Signed-off-by: Deluan <deluan@navidrome.org> * test(e2e): add tests for multi-library support and user access control Signed-off-by: Deluan <deluan@navidrome.org> * test(e2e): tests are fast, no need to skip on -short Signed-off-by: Deluan <deluan@navidrome.org> * address gemini comments Signed-off-by: Deluan <deluan@navidrome.org> * fix(tests): prevent MockDataStore from caching repos with stale context When RealDS is set, MockDataStore previously cached repository instances on first access, binding them to the initial caller's context. This meant repos created with an admin context would skip library filtering for all subsequent non-admin calls, silently masking access control bugs. Changed MockDataStore to delegate to RealDS on every call without caching, so each caller gets a fresh repo with the correct context. Removed the pre-warm calls in e2e setupTestDB that were working around the old caching behavior. * test(e2e): route subsonic tests through full HTTP middleware stack Replace direct router method calls with full HTTP round-trips via router.ServeHTTP(w, r) across all 15 e2e test files. Tests now exercise the complete chi middleware chain including postFormToQueryParams, checkRequiredParameters, authenticate, UpdateLastAccessMiddleware, getPlayer, and sendResponse/sendError serialization. New helpers (doReq, doReqWithUser, doRawReq, buildReq, parseJSONResponse) use plaintext password auth and JSON response format. Old helpers that injected context directly (newReq, newReqWithUser, newRawReq) are removed. Sharing tests now set conf.Server.EnableSharing before router creation to ensure sharing routes are registered. --------- Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-09 04:24:37 -09:00
conf.Server.MusicFolder = "fake:///music"
conf.Server.DevExternalScanner = false
// Restore DB to golden state (no scan needed)
restoreDB()
ds = &tests.MockDataStore{RealDS: persistence.New(db.Db())}
auth.Init(ds)
// Create the Subsonic Router with real DS + noop stubs
s := scanner.New(ctx, ds, artwork.NoopCacheWarmer(), events.NoopBroker(),
core.NewPlaylists(ds), metrics.NewNoopInstance())
router = subsonic.New(
ds,
noopArtwork{},
noopStreamer{},
noopArchiver{},
core.NewPlayers(ds),
noopProvider{},
s,
events.NoopBroker(),
core.NewPlaylists(ds),
noopPlayTracker{},
core.NewShare(ds),
playback.PlaybackServer(nil),
metrics.NewNoopInstance(),
)
}
// restoreDB restores all table data from the snapshot using ATTACH DATABASE.
// This is much faster than re-running the scanner for each test.
func restoreDB() {
sqlDB := db.Db()
_, err := sqlDB.Exec("PRAGMA foreign_keys = OFF")
Expect(err).ToNot(HaveOccurred())
_, err = sqlDB.Exec("ATTACH DATABASE ? AS snapshot", snapshotPath)
Expect(err).ToNot(HaveOccurred())
feat(server): implement FTS5-based full-text search (#5079) * build: add sqlite_fts5 build tag to enable FTS5 support * feat: add SearchBackend config option (default: fts) * feat: add buildFTS5Query for safe FTS5 query preprocessing * feat: add FTS5 search backend with config toggle, refactor legacy search - Add searchExprFunc type and getSearchExpr() for backend selection - Rename fullTextExpr to legacySearchExpr - Add ftsSearchExpr using FTS5 MATCH subquery - Update fullTextFilter in sql_restful.go to use configured backend * feat: add FTS5 migration with virtual tables, triggers, and search_participants Creates FTS5 virtual tables for media_file, album, and artist with unicode61 tokenizer and diacritic folding. Adds search_participants column, populates from JSON, and sets up INSERT/UPDATE/DELETE triggers. * feat: populate search_participants in PostMapArgs for FTS5 indexing * test: add FTS5 search integration tests * fix: exclude FTS5 virtual tables from e2e DB restore The restoreDB function iterates all tables in sqlite_master and runs DELETE + INSERT to reset state. FTS5 contentless virtual tables cannot be directly deleted from. Since triggers handle FTS5 sync automatically, simply skip tables matching *_fts and *_fts_* patterns. * build: add compile-time guard for sqlite_fts5 build tag Same pattern as netgo: compilation fails with a clear error if the sqlite_fts5 build tag is missing. * build: add sqlite_fts5 tag to reflex dev server config * build: extract GO_BUILD_TAGS variable in Makefile to avoid duplication * fix: strip leading * from FTS5 queries to prevent "unknown special query" error * feat: auto-append prefix wildcard to FTS5 search tokens for broader matching Every plain search token now gets a trailing * appended (e.g., "love" becomes "love*"), so searching for "love" also matches "lovelace", "lovely", etc. Quoted phrases are preserved as exact matches without wildcards. Results are ordered alphabetically by name/title, so shorter exact matches naturally appear first. * fix: clarify comments about FTS5 operator neutralization The comments said "strip" but the code lowercases operators to neutralize them (FTS5 operators are case-sensitive). Updated comments to accurately describe the behavior. * fix: use fmt.Sprintf for FTS5 phrase placeholders The previous encoding used rune('0'+index) which silently breaks with 10+ quoted phrases. Use fmt.Sprintf for arbitrary index support. * fix: validate and normalize SearchBackend config option Normalize the value to lowercase and fall back to "fts" with a log warning for unrecognized values. This prevents silent misconfiguration from typos like "FTS", "Legacy", or "fts5". * refactor: improve documentation for build tags and FTS5 requirements Signed-off-by: Deluan <deluan@navidrome.org> * refactor: convert FTS5 query and search backend normalization tests to DescribeTable format Signed-off-by: Deluan <deluan@navidrome.org> * fix: add sqlite_fts5 build tag to golangci configuration Signed-off-by: Deluan <deluan@navidrome.org> * feat: add UISearchDebounceMs configuration option and update related components Signed-off-by: Deluan <deluan@navidrome.org> * fix: fall back to legacy search when SearchFullString is enabled FTS5 is token-based and cannot match substrings within words, so getSearchExpr now returns legacySearchExpr when SearchFullString is true, regardless of SearchBackend setting. * fix: add sqlite_fts5 build tag to CI pipeline and Dockerfile * fix: add WHEN clauses to FTS5 AFTER UPDATE triggers Added WHEN clauses to the media_file_fts_au, album_fts_au, and artist_fts_au triggers so they only fire when FTS-indexed columns actually change. Previously, every row update (e.g., play count, rating, starred status) triggered an unnecessary delete+insert cycle in the FTS shadow tables. The WHEN clauses use IS NOT for NULL-safe comparison of each indexed column, avoiding FTS index churn for non-indexed updates. * feat: add SearchBackend configuration option to data and insights components Signed-off-by: Deluan <deluan@navidrome.org> * fix: enhance input sanitization for FTS5 by stripping additional punctuation and special characters Signed-off-by: Deluan <deluan@navidrome.org> * feat: add search_normalized column for punctuated name search (R.E.M., AC/DC) Add index-time normalization and query-time single-letter collapsing to fix FTS5 search for punctuated names. A new search_normalized column stores concatenated forms of punctuated words (e.g., "R.E.M." → "REM", "AC/DC" → "ACDC") and is indexed in FTS5 tables. At query time, runs of consecutive single letters (from dot-stripping) are collapsed into OR expressions like ("R E M" OR REM*) to match both the original tokens and the normalized form. This enables searching by "R.E.M.", "REM", "AC/DC", "ACDC", "A-ha", or "Aha" and finding the correct results. * refactor: simplify isSingleUnicodeLetter to avoid []rune allocation Use utf8.DecodeRuneInString to check for a single Unicode letter instead of converting the entire string to a []rune slice. * feat: define ftsSearchColumns for flexible FTS5 search column inclusion Signed-off-by: Deluan <deluan@navidrome.org> * feat: update collapseSingleLetterRuns to return quoted phrases for abbreviations Signed-off-by: Deluan <deluan@navidrome.org> * feat: implement extractPunctuatedWords to handle artist/album names with embedded punctuation Signed-off-by: Deluan <deluan@navidrome.org> * feat: implement extractPunctuatedWords to handle artist/album names with embedded punctuation Signed-off-by: Deluan <deluan@navidrome.org> * refactor: punctuated word handling to improve processing of artist/album names Signed-off-by: Deluan <deluan@navidrome.org> * feat: add CJK support for search queries with LIKE filters Signed-off-by: Deluan <deluan@navidrome.org> * feat: enhance FTS5 search by adding album version support and CJK handling Signed-off-by: Deluan <deluan@navidrome.org> * refactor: search configuration to use structured options Signed-off-by: Deluan <deluan@navidrome.org> * feat: enhance search functionality to support punctuation-only queries and update related tests Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-21 13:52:42 -09:00
rows, err := sqlDB.Query("SELECT name FROM main.sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '%_fts' AND name NOT LIKE '%_fts_%'")
test(subsonic): add comprehensive e2e test suite for Subsonic API (#5003) * test(e2e): add comprehensive tests for Subsonic API endpoints Signed-off-by: Deluan <deluan@navidrome.org> * fix(e2e): improve database handling and snapshot restoration in tests Signed-off-by: Deluan <deluan@navidrome.org> * test(e2e): add tests for album sharing and user isolation scenarios Signed-off-by: Deluan <deluan@navidrome.org> * test(e2e): add tests for multi-library support and user access control Signed-off-by: Deluan <deluan@navidrome.org> * test(e2e): tests are fast, no need to skip on -short Signed-off-by: Deluan <deluan@navidrome.org> * address gemini comments Signed-off-by: Deluan <deluan@navidrome.org> * fix(tests): prevent MockDataStore from caching repos with stale context When RealDS is set, MockDataStore previously cached repository instances on first access, binding them to the initial caller's context. This meant repos created with an admin context would skip library filtering for all subsequent non-admin calls, silently masking access control bugs. Changed MockDataStore to delegate to RealDS on every call without caching, so each caller gets a fresh repo with the correct context. Removed the pre-warm calls in e2e setupTestDB that were working around the old caching behavior. * test(e2e): route subsonic tests through full HTTP middleware stack Replace direct router method calls with full HTTP round-trips via router.ServeHTTP(w, r) across all 15 e2e test files. Tests now exercise the complete chi middleware chain including postFormToQueryParams, checkRequiredParameters, authenticate, UpdateLastAccessMiddleware, getPlayer, and sendResponse/sendError serialization. New helpers (doReq, doReqWithUser, doRawReq, buildReq, parseJSONResponse) use plaintext password auth and JSON response format. Old helpers that injected context directly (newReq, newReqWithUser, newRawReq) are removed. Sharing tests now set conf.Server.EnableSharing before router creation to ensure sharing routes are registered. --------- Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-09 04:24:37 -09:00
Expect(err).ToNot(HaveOccurred())
var tables []string
for rows.Next() {
var name string
Expect(rows.Scan(&name)).To(Succeed())
tables = append(tables, name)
}
Expect(rows.Err()).ToNot(HaveOccurred())
rows.Close()
for _, table := range tables {
// Table names come from sqlite_master, not user input, so concatenation is safe here
_, err = sqlDB.Exec(`DELETE FROM main."` + table + `"`) //nolint:gosec
Expect(err).ToNot(HaveOccurred())
_, err = sqlDB.Exec(`INSERT INTO main."` + table + `" SELECT * FROM snapshot."` + table + `"`) //nolint:gosec
Expect(err).ToNot(HaveOccurred())
}
_, err = sqlDB.Exec("DETACH DATABASE snapshot")
Expect(err).ToNot(HaveOccurred())
_, err = sqlDB.Exec("PRAGMA foreign_keys = ON")
Expect(err).ToNot(HaveOccurred())
}