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 ( ) )
2026-02-10 07:25:17 -09:00
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 ( ) )
}