2025-03-18 15:12:07 -08:00
|
|
|
package conf_test
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
2026-03-28 09:23:03 -08:00
|
|
|
"os"
|
2025-03-18 15:12:07 -08:00
|
|
|
"path/filepath"
|
|
|
|
|
"testing"
|
|
|
|
|
|
2025-05-22 16:50:15 -08:00
|
|
|
"github.com/navidrome/navidrome/conf"
|
2025-03-18 15:12:07 -08:00
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
|
|
|
. "github.com/onsi/gomega"
|
|
|
|
|
"github.com/spf13/viper"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestConfiguration(t *testing.T) {
|
|
|
|
|
RegisterFailHandler(Fail)
|
|
|
|
|
RunSpecs(t, "Configuration Suite")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var _ = Describe("Configuration", func() {
|
|
|
|
|
BeforeEach(func() {
|
|
|
|
|
// Reset viper configuration
|
|
|
|
|
viper.Reset()
|
2025-05-22 16:50:15 -08:00
|
|
|
conf.SetViperDefaults()
|
2025-03-18 15:12:07 -08:00
|
|
|
viper.SetDefault("datafolder", GinkgoT().TempDir())
|
|
|
|
|
viper.SetDefault("loglevel", "error")
|
2025-05-22 16:50:15 -08:00
|
|
|
conf.ResetConf()
|
2026-03-28 09:23:03 -08:00
|
|
|
|
|
|
|
|
// Panic instead of exiting on fatal errors to allow testing error conditions
|
|
|
|
|
DeferCleanup(conf.SetLogFatal(func(args ...any) {
|
|
|
|
|
panic(fmt.Sprint(args...))
|
|
|
|
|
}))
|
2025-03-18 15:12:07 -08:00
|
|
|
})
|
|
|
|
|
|
2026-01-29 09:05:51 -09:00
|
|
|
Describe("ParseLanguages", func() {
|
|
|
|
|
It("parses single language", func() {
|
|
|
|
|
Expect(conf.ParseLanguages("en")).To(Equal([]string{"en"}))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("parses multiple comma-separated languages", func() {
|
|
|
|
|
Expect(conf.ParseLanguages("pt,en")).To(Equal([]string{"pt", "en"}))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("trims whitespace from languages", func() {
|
|
|
|
|
Expect(conf.ParseLanguages(" pt , en ")).To(Equal([]string{"pt", "en"}))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("returns default 'en' when empty", func() {
|
|
|
|
|
Expect(conf.ParseLanguages("")).To(Equal([]string{"en"}))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("returns default 'en' when only whitespace", func() {
|
|
|
|
|
Expect(conf.ParseLanguages(" ")).To(Equal([]string{"en"}))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("handles multiple languages with various spacing", func() {
|
|
|
|
|
Expect(conf.ParseLanguages("ja, pt, en")).To(Equal([]string{"ja", "pt", "en"}))
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-23 16:28:38 -09:00
|
|
|
Describe("ValidateURL", func() {
|
|
|
|
|
It("accepts a valid http URL", func() {
|
|
|
|
|
fn := conf.ValidateURL("TestOption", "http://example.com/path")
|
|
|
|
|
Expect(fn()).To(Succeed())
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("accepts a valid https URL", func() {
|
|
|
|
|
fn := conf.ValidateURL("TestOption", "https://example.com/path")
|
|
|
|
|
Expect(fn()).To(Succeed())
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("rejects a URL with no scheme", func() {
|
|
|
|
|
fn := conf.ValidateURL("TestOption", "example.com/path")
|
|
|
|
|
Expect(fn()).To(MatchError(ContainSubstring("invalid scheme")))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("rejects a URL with an unsupported scheme", func() {
|
|
|
|
|
fn := conf.ValidateURL("TestOption", "javascript://example.com/path")
|
|
|
|
|
Expect(fn()).To(MatchError(ContainSubstring("invalid scheme")))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("accepts an empty URL (optional config)", func() {
|
|
|
|
|
fn := conf.ValidateURL("TestOption", "")
|
|
|
|
|
Expect(fn()).To(Succeed())
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("includes the option name in the error message", func() {
|
|
|
|
|
fn := conf.ValidateURL("MyOption", "ftp://example.com")
|
|
|
|
|
Expect(fn()).To(MatchError(ContainSubstring("MyOption")))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("rejects a URL that cannot be parsed", func() {
|
|
|
|
|
fn := conf.ValidateURL("TestOption", "://invalid")
|
|
|
|
|
Expect(fn()).To(HaveOccurred())
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("rejects a URL without a host", func() {
|
|
|
|
|
fn := conf.ValidateURL("TestOption", "http:///path")
|
|
|
|
|
Expect(fn()).To(MatchError(ContainSubstring("non-empty host is required")))
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
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
|
|
|
DescribeTable("NormalizeSearchBackend",
|
|
|
|
|
func(input, expected string) {
|
|
|
|
|
Expect(conf.NormalizeSearchBackend(input)).To(Equal(expected))
|
|
|
|
|
},
|
|
|
|
|
Entry("accepts 'fts'", "fts", "fts"),
|
|
|
|
|
Entry("accepts 'legacy'", "legacy", "legacy"),
|
|
|
|
|
Entry("normalizes 'FTS' to lowercase", "FTS", "fts"),
|
|
|
|
|
Entry("normalizes 'Legacy' to lowercase", "Legacy", "legacy"),
|
|
|
|
|
Entry("trims whitespace", " fts ", "fts"),
|
|
|
|
|
Entry("falls back to 'fts' for 'fts5'", "fts5", "fts"),
|
|
|
|
|
Entry("falls back to 'fts' for unrecognized values", "invalid", "fts"),
|
|
|
|
|
Entry("falls back to 'fts' for empty string", "", "fts"),
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-28 09:17:31 -08:00
|
|
|
DescribeTable("ToPascalCase",
|
|
|
|
|
func(input, expected string) {
|
|
|
|
|
Expect(conf.ToPascalCase(input)).To(Equal(expected))
|
|
|
|
|
},
|
|
|
|
|
Entry("simple key", "address", "Address"),
|
|
|
|
|
Entry("dotted key", "scanner.schedule", "Scanner.Schedule"),
|
|
|
|
|
Entry("already capitalized", "Address", "Address"),
|
|
|
|
|
Entry("multi-segment", "lastfm.enabled", "Lastfm.Enabled"),
|
|
|
|
|
Entry("empty string", "", ""),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
Describe("remapEnvVarKeysFromConfig", func() {
|
|
|
|
|
BeforeEach(func() {
|
|
|
|
|
viper.Reset()
|
|
|
|
|
conf.SetViperDefaults()
|
|
|
|
|
viper.SetDefault("datafolder", GinkgoT().TempDir())
|
|
|
|
|
viper.SetDefault("loglevel", "error")
|
|
|
|
|
conf.ResetConf()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("remaps ND_-prefixed keys to canonical keys", func() {
|
|
|
|
|
filename := filepath.Join("testdata", "cfg_nd_keys.toml")
|
|
|
|
|
conf.InitConfig(filename, false)
|
|
|
|
|
conf.Load(true)
|
|
|
|
|
|
|
|
|
|
Expect(conf.Server.Address).To(Equal("127.0.0.1"))
|
|
|
|
|
Expect(conf.Server.Port).To(Equal(4531))
|
|
|
|
|
Expect(conf.Server.Scanner.Schedule).To(Equal("@every 1h"))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("exits with fatal error when both ND_ and canonical key exist", func() {
|
|
|
|
|
filename := filepath.Join("testdata", "cfg_nd_conflict.toml")
|
|
|
|
|
conf.InitConfig(filename, false)
|
|
|
|
|
|
|
|
|
|
Expect(func() { conf.Load(true) }).To(PanicWith(And(
|
|
|
|
|
ContainSubstring("ND_ADDRESS"),
|
|
|
|
|
ContainSubstring("Address"),
|
|
|
|
|
ContainSubstring("only needed for environment variables"),
|
|
|
|
|
)))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("does nothing when no ND_ keys are present", func() {
|
|
|
|
|
filename := filepath.Join("testdata", "cfg.toml")
|
|
|
|
|
conf.InitConfig(filename, false)
|
|
|
|
|
conf.Load(true)
|
|
|
|
|
|
|
|
|
|
// Verify normal config loading still works
|
|
|
|
|
Expect(conf.Server.MusicFolder).To(Equal("/toml/music"))
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-28 09:23:03 -08:00
|
|
|
Describe("logFatal", func() {
|
|
|
|
|
var invalidPath string
|
|
|
|
|
BeforeEach(func() {
|
|
|
|
|
viper.Reset()
|
|
|
|
|
conf.SetViperDefaults()
|
|
|
|
|
viper.SetDefault("loglevel", "error")
|
|
|
|
|
conf.ResetConf()
|
|
|
|
|
|
|
|
|
|
// Create a file so that any path under it is invalid on all OSes
|
|
|
|
|
f, err := os.CreateTemp(GinkgoT().TempDir(), "blocker")
|
|
|
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
|
f.Close()
|
|
|
|
|
invalidPath = filepath.Join(f.Name(), "subdir")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("is called when LoadFromFile gets an invalid config file", func() {
|
|
|
|
|
Expect(func() {
|
|
|
|
|
conf.LoadFromFile(filepath.Join(invalidPath, "file.toml"))
|
|
|
|
|
}).To(PanicWith(ContainSubstring("Error reading config file")))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("is called when LogFile path is not writable", func() {
|
|
|
|
|
viper.SetDefault("datafolder", GinkgoT().TempDir())
|
|
|
|
|
viper.SetDefault("logfile", filepath.Join(invalidPath, "log.txt"))
|
|
|
|
|
Expect(func() {
|
|
|
|
|
conf.Load(true)
|
refactor(conf): replace eager dir creation with lazy Dir type (#5495)
* feat(conf): add Dir type with lazy directory creation
Introduces the Dir type that wraps a directory path string and defers
os.MkdirAll until the first call to Path() or MustPath(), using sync.Once
to ensure the creation happens exactly once. Implements fmt.Stringer,
encoding.TextMarshaler, and encoding.TextUnmarshaler for config integration.
Includes Ginkgo/Gomega tests covering all methods and error paths.
* refactor(conf): replace eager dir creation with lazy Dir type
Change DataFolder, CacheFolder, Plugins.Folder, and Backup.Path from
string to Dir. Remove all os.MkdirAll calls from Load() so directories
are created lazily on first Path()/MustPath() call. Artwork folder
creation was already handled at point-of-use in image_upload.go.
Add SnapshotConfig() to conf package for safe test config save/restore
that avoids copying sync.Once inside Dir fields. Fix copy-lock vet
warning in nativeapi/config.go by marshalling pointer instead of value.
* refactor(conf): migrate tests and db init to lazy Dir type
Update all test files to use conf.NewDir() for Dir field assignments.
Ensure DataFolder is created lazily when the database is first opened
in db.Db(). Remove eager directory creation from conf.Load() tests.
* fix(conf): address review findings for Dir type
- Use os.ModePerm for DataFolder/CacheFolder (was 0700, should match
original behavior). Add NewDirWithPerm for PluginsFolder (0700).
- Use Path() instead of MustPath() in db.Prune() to avoid logFatal
from background cron job.
- Panic on marshal/unmarshal errors in SnapshotConfig (test helper).
- Clean up redundant String()/MustPath() calls in plugin manager.
- Remove dead code in dir_test.go.
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(conf): add GoString to Dir for clean config dump output
Implement fmt.GoStringer on Dir so pretty.Sprintf shows the path
string instead of internal struct fields (sync.Once, perm, err).
Also add TODO comment to configtest about removing the indirection.
* fix(dir): improve error logging in MustPath method
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(tests): remove redundant tests for unwritable DataFolder and CacheFolder
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(conf): address PR review feedback
- Ensure Plugins.Folder always uses 0700, even when user-configured
(previously only the derived default got restrictive permissions).
- Create LogFile parent directory before opening, so LogFile paths
inside a not-yet-created DataFolder work correctly.
---------
Signed-off-by: Deluan <deluan@navidrome.org>
2026-05-13 12:44:22 -08:00
|
|
|
}).To(PanicWith(ContainSubstring("Error creating log file directory")))
|
2026-03-28 09:23:03 -08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("is called when BaseURL is invalid", func() {
|
|
|
|
|
viper.SetDefault("datafolder", GinkgoT().TempDir())
|
|
|
|
|
viper.SetDefault("baseurl", "://invalid")
|
|
|
|
|
Expect(func() {
|
|
|
|
|
conf.Load(true)
|
|
|
|
|
}).To(PanicWith(ContainSubstring("Invalid BaseURL")))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-12 07:16:00 -08:00
|
|
|
Describe("ValidateMaxImageUploadSize", func() {
|
|
|
|
|
BeforeEach(func() {
|
|
|
|
|
viper.Reset()
|
|
|
|
|
conf.SetViperDefaults()
|
|
|
|
|
viper.SetDefault("datafolder", GinkgoT().TempDir())
|
|
|
|
|
viper.SetDefault("loglevel", "error")
|
|
|
|
|
conf.ResetConf()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
DescribeTable("accepts valid size values",
|
|
|
|
|
func(input string) {
|
|
|
|
|
conf.Server.MaxImageUploadSize = input
|
|
|
|
|
Expect(conf.ValidateMaxImageUploadSize()).To(Succeed())
|
|
|
|
|
},
|
|
|
|
|
Entry("megabytes", "10MB"),
|
|
|
|
|
Entry("gigabytes", "1GB"),
|
|
|
|
|
Entry("raw bytes", "10485760"),
|
|
|
|
|
Entry("mebibytes", "10MiB"),
|
|
|
|
|
Entry("lower case", "50mb"),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
DescribeTable("rejects invalid size values",
|
|
|
|
|
func(input string) {
|
|
|
|
|
conf.Server.MaxImageUploadSize = input
|
|
|
|
|
Expect(conf.ValidateMaxImageUploadSize()).To(MatchError(ContainSubstring("invalid MaxImageUploadSize")))
|
|
|
|
|
},
|
|
|
|
|
Entry("garbage string", "not-a-size"),
|
|
|
|
|
Entry("negative-looking", "-10MB"),
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-21 17:27:54 -08:00
|
|
|
Describe("EnforceNonRootUser", func() {
|
|
|
|
|
It("defaults to false", func() {
|
|
|
|
|
conf.Load(true)
|
|
|
|
|
|
|
|
|
|
Expect(conf.Server.EnforceNonRootUser).To(BeFalse())
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("allows startup for non-root users when enabled", func() {
|
|
|
|
|
DeferCleanup(conf.SetRuntimeInfoForTest("linux", 1000))
|
|
|
|
|
viper.Set("enforcenonrootuser", true)
|
|
|
|
|
|
|
|
|
|
conf.Load(true)
|
|
|
|
|
|
|
|
|
|
Expect(conf.Server.EnforceNonRootUser).To(BeTrue())
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("exits when enabled and running as root without having created a data folder", func() {
|
|
|
|
|
// Create a path that doesn't exist yet
|
|
|
|
|
tempBase := GinkgoT().TempDir()
|
|
|
|
|
nonExistentDataFolder := filepath.Join(tempBase, "nonexistent", "data")
|
|
|
|
|
DeferCleanup(conf.SetRuntimeInfoForTest("linux", 0))
|
|
|
|
|
viper.Set("enforcenonrootuser", true)
|
|
|
|
|
viper.Set("datafolder", nonExistentDataFolder)
|
|
|
|
|
|
|
|
|
|
// Attempt to load config as root user - should fail before creating directories
|
|
|
|
|
Expect(func() {
|
|
|
|
|
conf.Load(true)
|
|
|
|
|
}).To(PanicWith(ContainSubstring("EnforceNonRootUser is enabled but Navidrome is running as root")))
|
|
|
|
|
|
|
|
|
|
// Verify that the data folder was NOT created
|
|
|
|
|
Expect(nonExistentDataFolder).ToNot(BeAnExistingFile())
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("is a no-op on non-unix platforms", func() {
|
|
|
|
|
DeferCleanup(conf.SetRuntimeInfoForTest("windows", 0))
|
|
|
|
|
viper.Set("enforcenonrootuser", true)
|
|
|
|
|
|
|
|
|
|
conf.Load(true)
|
|
|
|
|
|
|
|
|
|
Expect(conf.Server.EnforceNonRootUser).To(BeTrue())
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
2025-03-18 15:12:07 -08:00
|
|
|
DescribeTable("should load configuration from",
|
|
|
|
|
func(format string) {
|
|
|
|
|
filename := filepath.Join("testdata", "cfg."+format)
|
|
|
|
|
|
|
|
|
|
// Initialize config with the test file
|
2025-11-29 07:44:24 -09:00
|
|
|
conf.InitConfig(filename, false)
|
2025-03-18 15:12:07 -08:00
|
|
|
// Load the configuration (with noConfigDump=true)
|
2025-05-22 16:50:15 -08:00
|
|
|
conf.Load(true)
|
2025-03-18 15:12:07 -08:00
|
|
|
|
|
|
|
|
// Execute the format-specific assertions
|
2025-05-22 16:50:15 -08:00
|
|
|
Expect(conf.Server.MusicFolder).To(Equal(fmt.Sprintf("/%s/music", format)))
|
|
|
|
|
Expect(conf.Server.UIWelcomeMessage).To(Equal("Welcome " + format))
|
|
|
|
|
Expect(conf.Server.Tags["custom"].Aliases).To(Equal([]string{format, "test"}))
|
2025-11-22 16:14:44 -09:00
|
|
|
Expect(conf.Server.Tags["artist"].Split).To(Equal([]string{";"}))
|
2025-03-18 15:12:07 -08:00
|
|
|
|
2025-12-02 08:01:48 -09:00
|
|
|
// Check deprecated option mapping
|
|
|
|
|
Expect(conf.Server.ExtAuth.UserHeader).To(Equal("X-Auth-User"))
|
|
|
|
|
|
2025-03-18 15:12:07 -08:00
|
|
|
// The config file used should be the one we created
|
2025-05-22 16:50:15 -08:00
|
|
|
Expect(conf.Server.ConfigFile).To(Equal(filename))
|
2025-03-18 15:12:07 -08:00
|
|
|
},
|
|
|
|
|
Entry("TOML format", "toml"),
|
|
|
|
|
Entry("YAML format", "yaml"),
|
|
|
|
|
Entry("INI format", "ini"),
|
|
|
|
|
Entry("JSON format", "json"),
|
|
|
|
|
)
|
|
|
|
|
})
|