mirror of
https://github.com/navidrome/navidrome.git
synced 2026-11-04 01:08:54 +01:00
validatePlaylistsPath checked the raw pattern, while InPath matches the
converted one. On Windows, 'Playlists\{rock,jazz}' failed validation
and stopped the server, although InPath matches it, and
'Playlists\[Mix' passed validation but is invalid once converted.
Validation now uses conf.PlaylistsPathPatterns, like InPath.
Tests load real TOML files through conf.LoadFromFile.
636 lines
21 KiB
Go
636 lines
21 KiB
Go
package conf_test
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/conf/configtest"
|
|
"github.com/navidrome/navidrome/consts"
|
|
"github.com/navidrome/navidrome/log"
|
|
. "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()
|
|
conf.SetViperDefaults()
|
|
viper.SetDefault("datafolder", GinkgoT().TempDir())
|
|
viper.SetDefault("loglevel", "error")
|
|
conf.ResetConf()
|
|
|
|
// Panic instead of exiting on fatal errors to allow testing error conditions
|
|
DeferCleanup(conf.SetLogFatal(func(args ...any) {
|
|
panic(fmt.Sprint(args...))
|
|
}))
|
|
})
|
|
|
|
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"}))
|
|
})
|
|
})
|
|
|
|
Describe("scheduled DB analysis", func() {
|
|
It("is enabled by default", func() {
|
|
conf.Load(true)
|
|
Expect(conf.Server.EnableScheduledDBAnalyze).To(BeTrue())
|
|
})
|
|
|
|
It("can be disabled", func() {
|
|
viper.Set("enablescheduleddbanalyze", false)
|
|
conf.Load(true)
|
|
Expect(conf.Server.EnableScheduledDBAnalyze).To(BeFalse())
|
|
})
|
|
})
|
|
|
|
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")))
|
|
})
|
|
})
|
|
|
|
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"),
|
|
)
|
|
|
|
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"))
|
|
})
|
|
})
|
|
|
|
Describe("unknownConfigKeys", func() {
|
|
BeforeEach(func() {
|
|
viper.Reset()
|
|
conf.SetViperDefaults()
|
|
viper.SetDefault("datafolder", GinkgoT().TempDir())
|
|
viper.SetDefault("loglevel", "error")
|
|
conf.ResetConf()
|
|
})
|
|
|
|
It("reports misplaced and misspelled options, as spelled in the config file", func() {
|
|
conf.InitConfig(filepath.Join("testdata", "cfg_unknown_keys.toml"), false)
|
|
conf.Load(true)
|
|
|
|
Expect(conf.UnknownConfigKeys()).To(ConsistOf(
|
|
"ArtistSplitExceptions", "EnableDownlods", "Whatever.Foo",
|
|
))
|
|
})
|
|
|
|
DescribeTable("recovers the original casing in all supported formats",
|
|
func(file string) {
|
|
conf.InitConfig(filepath.Join("testdata", file), false)
|
|
conf.Load(true)
|
|
|
|
Expect(conf.UnknownConfigKeys()).To(ConsistOf("NotAnOption"))
|
|
},
|
|
Entry("TOML", "cfg_unknown_casing.toml"),
|
|
Entry("YAML", "cfg_unknown_casing.yaml"),
|
|
Entry("JSON", "cfg_unknown_casing.json"),
|
|
Entry("INI", "cfg_unknown_casing.ini"),
|
|
)
|
|
|
|
It("does not report valid, deprecated or free-form keys", func() {
|
|
conf.InitConfig(filepath.Join("testdata", "cfg.toml"), false)
|
|
conf.Load(true)
|
|
|
|
Expect(conf.UnknownConfigKeys()).To(BeEmpty())
|
|
})
|
|
|
|
It("does not report the [default] section of INI files", func() {
|
|
conf.InitConfig(filepath.Join("testdata", "cfg.ini"), false)
|
|
conf.Load(true)
|
|
|
|
Expect(conf.UnknownConfigKeys()).To(BeEmpty())
|
|
})
|
|
|
|
DescribeTable("SuggestOptions",
|
|
func(key string, expected []string) {
|
|
Expect(conf.SuggestOptions(key)).To(Equal(expected))
|
|
},
|
|
Entry("suggests the section of a misplaced option", "artistsplitexceptions",
|
|
[]string{"Scanner.ArtistSplitExceptions"}),
|
|
Entry("suggests the section of a misplaced nested option", "backup.fuzzythreshold",
|
|
[]string{"Matcher.FuzzyThreshold"}),
|
|
Entry("suggests every section defining the option", "schedule",
|
|
[]string{"Backup.Schedule", "Scanner.Schedule"}),
|
|
Entry("suggests nothing for a typo", "enabledownlods", nil),
|
|
)
|
|
|
|
It("does not report ND_-prefixed keys, as they are remapped", func() {
|
|
conf.InitConfig(filepath.Join("testdata", "cfg_nd_keys.toml"), false)
|
|
conf.Load(true)
|
|
|
|
Expect(conf.UnknownConfigKeys()).To(BeEmpty())
|
|
})
|
|
|
|
It("reports ND_-prefixed keys that remap to no known option", func() {
|
|
conf.InitConfig(filepath.Join("testdata", "cfg_nd_bogus.toml"), false)
|
|
conf.Load(true)
|
|
|
|
Expect(conf.UnknownConfigKeys()).To(ConsistOf("ND_TOTALLY_BOGUS_OPTION"))
|
|
Expect(conf.Server.Scanner.Schedule).To(Equal("@every 1h"))
|
|
})
|
|
|
|
It("migrates every deprecated option that has a replacement", func() {
|
|
conf.InitConfig(filepath.Join("testdata", "cfg_deprecated_search.toml"), false)
|
|
conf.Load(true)
|
|
|
|
Expect(conf.Server.Search.FullString).To(BeTrue())
|
|
Expect(conf.UnknownConfigKeys()).To(BeEmpty())
|
|
})
|
|
|
|
It("warns about each unrecognized option at startup", func() {
|
|
var logBuf bytes.Buffer
|
|
log.SetOutput(&logBuf)
|
|
DeferCleanup(func() { log.SetOutput(GinkgoWriter) })
|
|
|
|
conf.InitConfig(filepath.Join("testdata", "cfg_warning_output.toml"), false)
|
|
conf.Load(true)
|
|
|
|
Expect(logBuf.String()).To(ContainSubstring(
|
|
"Option 'ArtistSplitExceptions' is not recognized and will be ignored. " +
|
|
"Did you mean 'Scanner.ArtistSplitExceptions'?"))
|
|
Expect(logBuf.String()).To(ContainSubstring(
|
|
"Option 'EnableDownlods' is not recognized and will be ignored"))
|
|
Expect(logBuf.String()).ToNot(ContainSubstring("ArtistJoiner"))
|
|
})
|
|
|
|
Context("with runtime-computed and removed options in the config", func() {
|
|
BeforeEach(func() {
|
|
conf.InitConfig(filepath.Join("testdata", "cfg_runtime_fields.toml"), false)
|
|
conf.Load(true)
|
|
})
|
|
|
|
It("reports values computed during Load, which the config cannot set", func() {
|
|
Expect(conf.UnknownConfigKeys()).To(ContainElements("ConfigFile", "LastFM.Languages"))
|
|
})
|
|
|
|
It("never suggests a removed option", func() {
|
|
Expect(conf.SuggestOptions("id")).To(BeEmpty())
|
|
})
|
|
|
|
It("keeps an explicit replacement over the deprecated value", func() {
|
|
Expect(conf.Server.Search.FullString).To(BeFalse())
|
|
})
|
|
})
|
|
})
|
|
|
|
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)
|
|
}).To(PanicWith(ContainSubstring("Error creating log file directory")))
|
|
})
|
|
|
|
It("creates the log file readable only by the owner", func() {
|
|
if runtime.GOOS == "windows" {
|
|
Skip("file modes are not enforced on Windows")
|
|
}
|
|
logFile := filepath.Join(GinkgoT().TempDir(), "navidrome.log")
|
|
viper.SetDefault("datafolder", GinkgoT().TempDir())
|
|
viper.SetDefault("logfile", logFile)
|
|
DeferCleanup(log.SetOutput, os.Stderr)
|
|
conf.Load(true)
|
|
|
|
info, err := os.Stat(logFile)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(info.Mode().Perm()).To(Equal(os.FileMode(0600)))
|
|
})
|
|
|
|
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")))
|
|
})
|
|
|
|
})
|
|
|
|
Describe("ValidateByteSize", func() {
|
|
DescribeTable("accepts valid size values",
|
|
func(input string) {
|
|
Expect(conf.ValidateByteSize("MaxImageSize", input)()).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) {
|
|
Expect(conf.ValidateByteSize("MaxImageSize", input)()).To(MatchError(ContainSubstring("invalid MaxImageSize")))
|
|
},
|
|
Entry("garbage string", "not-a-size"),
|
|
Entry("negative-looking", "-10MB"),
|
|
Entry("zero", "0"),
|
|
Entry("zero with unit", "0MB"),
|
|
Entry("overflows int64", "9223372036854775808"),
|
|
)
|
|
})
|
|
|
|
Describe("PlaylistsPathPatterns", func() {
|
|
It("splits the list with the OS list separator", func() {
|
|
conf.Server.PlaylistsPath = "." + string(filepath.ListSeparator) + "Playlists/**"
|
|
Expect(conf.PlaylistsPathPatterns()).To(Equal([]string{".", "Playlists/**"}))
|
|
})
|
|
|
|
DescribeTable("converts a Windows pattern to slash form",
|
|
func(pattern, expected string) {
|
|
Expect(conf.ToGlobPattern(pattern, true)).To(Equal(expected))
|
|
},
|
|
Entry("separator", `Playlists\navidrome`, "Playlists/navidrome"),
|
|
Entry("separator before **", `Playlists\**`, "Playlists/**"),
|
|
Entry("separator before braces", `Playlists\{rock,jazz}`, "Playlists/{rock,jazz}"),
|
|
Entry("trailing separator", `Playlists\`, "Playlists/"),
|
|
Entry("escaped brackets", `\[Mix\]`, `\[Mix\]`),
|
|
Entry("escaped brackets after a slash", `Playlists/\[Mix\]`, `Playlists/\[Mix\]`),
|
|
Entry("escaped brackets after a separator", `Playlists\\[Mix\]`, `Playlists/\[Mix\]`),
|
|
Entry("character class", `[[]Mix]`, `[[]Mix]`),
|
|
Entry("separator before a character class", `Playlists\[[]Mix]`, "Playlists/[[]Mix]"),
|
|
Entry("separator before a bracket range", `Playlists\[ab]`, "Playlists/[ab]"),
|
|
Entry("escaped braces", `\{Mix\}`, `\{Mix\}`),
|
|
Entry("escaped braces after a separator", `Playlists\\{Mix\}`, `Playlists/\{Mix\}`),
|
|
Entry("lone escaped closing bracket", `Mix\]`, `Mix\]`),
|
|
// Ambiguous: an escaped pair wins, so a separator right before it must be '/' or '\\'
|
|
Entry("escaped pair right after a name", `Playlists\[Mix\]`, `Playlists\[Mix\]`),
|
|
Entry("forward slashes", "Playlists/navidrome", "Playlists/navidrome"),
|
|
)
|
|
|
|
DescribeTable("keeps a non-Windows pattern as is",
|
|
func(pattern string) {
|
|
Expect(conf.ToGlobPattern(pattern, false)).To(Equal(pattern))
|
|
},
|
|
Entry("backslash escape", `Playlists\navidrome`),
|
|
Entry("escaped brackets", `\[Mix\]`),
|
|
Entry("escaped star", `\*`),
|
|
)
|
|
})
|
|
|
|
Describe("PlaylistsPath validation", func() {
|
|
// A real TOML file with literal (single-quoted) strings, like Windows users write paths
|
|
loadConfig := func(pattern string) (err error) {
|
|
file := filepath.Join(GinkgoT().TempDir(), "navidrome.toml")
|
|
Expect(os.WriteFile(file, []byte("PlaylistsPath = '"+pattern+"'\n"), 0600)).To(Succeed())
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
err = fmt.Errorf("%v", r)
|
|
}
|
|
}()
|
|
conf.LoadFromFile(file)
|
|
return nil
|
|
}
|
|
|
|
DescribeTable("accepts exactly the patterns InPath can use",
|
|
func(pattern string, accepted bool) {
|
|
err := loadConfig(pattern)
|
|
if accepted {
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(conf.Server.PlaylistsPath).To(Equal(pattern))
|
|
} else {
|
|
Expect(err).To(MatchError(ContainSubstring("invalid PlaylistsPath")))
|
|
}
|
|
},
|
|
Entry("backslash path (issue #6276)", `Playlists\navidrome`, true),
|
|
Entry("escaped brackets", `\[Mix\]`, true),
|
|
Entry("backslash before a bracket range", `Playlists\[ab]`, true),
|
|
Entry("unterminated character class", `[Mix`, false),
|
|
Entry("unterminated brace alternatives", `{rock,jazz`, false),
|
|
Entry("backslash before brace alternatives", `Playlists\{rock,jazz}`, runtime.GOOS == "windows"),
|
|
Entry("backslash before an unterminated class", `Playlists\[Mix`, runtime.GOOS != "windows"),
|
|
)
|
|
})
|
|
|
|
Describe("MaxImageSize floor", func() {
|
|
BeforeEach(func() {
|
|
viper.Reset()
|
|
conf.SetViperDefaults()
|
|
viper.SetDefault("datafolder", GinkgoT().TempDir())
|
|
viper.SetDefault("loglevel", "error")
|
|
conf.ResetConf()
|
|
})
|
|
|
|
It("is raised to MaxImageUploadSize when configured lower", func() {
|
|
viper.SetDefault("maximagesize", "5MB")
|
|
viper.SetDefault("maximageuploadsize", "50MB")
|
|
conf.Load(true)
|
|
Expect(conf.Server.MaxImageSize).To(Equal("50MB"))
|
|
})
|
|
|
|
It("keeps a larger MaxImageSize unchanged", func() {
|
|
viper.SetDefault("maximagesize", "30MB")
|
|
conf.Load(true)
|
|
Expect(conf.Server.MaxImageSize).To(Equal("30MB"))
|
|
})
|
|
})
|
|
|
|
Describe("Scanner.Extractor", func() {
|
|
BeforeEach(func() {
|
|
viper.Reset()
|
|
conf.SetViperDefaults()
|
|
viper.SetDefault("datafolder", GinkgoT().TempDir())
|
|
viper.SetDefault("loglevel", "error")
|
|
conf.ResetConf()
|
|
})
|
|
|
|
It("falls back to taglib for an unknown extractor", func() {
|
|
viper.SetDefault("scanner.extractor", "ffmpeg")
|
|
conf.Load(true)
|
|
Expect(conf.Server.Scanner.Extractor).To(Equal("taglib"))
|
|
})
|
|
|
|
It("keeps taglib", func() {
|
|
conf.Load(true)
|
|
Expect(conf.Server.Scanner.Extractor).To(Equal("taglib"))
|
|
})
|
|
})
|
|
|
|
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())
|
|
})
|
|
})
|
|
|
|
DescribeTable("should load configuration from",
|
|
func(format string) {
|
|
filename := filepath.Join("testdata", "cfg."+format)
|
|
|
|
// Initialize config with the test file
|
|
conf.InitConfig(filename, false)
|
|
// Load the configuration (with noConfigDump=true)
|
|
conf.Load(true)
|
|
|
|
// Execute the format-specific assertions
|
|
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"}))
|
|
Expect(conf.Server.Tags["artist"].Split).To(Equal([]string{";"}))
|
|
|
|
// Check deprecated option mapping
|
|
Expect(conf.Server.ExtAuth.UserHeader).To(Equal("X-Auth-User"))
|
|
|
|
// The config file used should be the one we created
|
|
Expect(conf.Server.ConfigFile).To(Equal(filename))
|
|
},
|
|
Entry("TOML format", "toml"),
|
|
Entry("YAML format", "yaml"),
|
|
Entry("INI format", "ini"),
|
|
Entry("JSON format", "json"),
|
|
)
|
|
|
|
It("should use default values for negative duration fields", func() {
|
|
filename := filepath.Join("testdata", "invalid_duration.toml")
|
|
conf.InitConfig(filename, false)
|
|
conf.Load(true)
|
|
|
|
server := conf.Server
|
|
Expect(server.SessionTimeout).To(Equal(consts.DefaultSessionTimeout))
|
|
Expect(server.SmartPlaylistRefreshDelay).To(Equal(consts.DefaultSmartRefresh))
|
|
Expect(server.DefaultShareExpiration).To(Equal(consts.DefaultShareExpiration))
|
|
Expect(server.UIPlaybackReportInterval).To(Equal(consts.DefaultUIPlaybackReportInterval))
|
|
Expect(server.AuthWindowLength).To(Equal(consts.DefaultAuthWindowLength))
|
|
Expect(server.Scanner.WatcherWait).To(Equal(consts.DefaultWatcherWait))
|
|
|
|
Expect(server.DevActivityPanelUpdateRate).To(Equal(consts.DefaultActivityPanelUpdateRate))
|
|
Expect(server.DevArtworkThrottleBacklogTimeout).To(Equal(consts.RequestThrottleBacklogTimeout))
|
|
Expect(server.DevArtistInfoTimeToLive).To(Equal(consts.ArtistInfoTimeToLive))
|
|
Expect(server.DevAlbumInfoTimeToLive).To(Equal(consts.AlbumInfoTimeToLive))
|
|
Expect(server.DevInsightsInitialDelay).To(Equal(consts.InsightsInitialDelay))
|
|
Expect(server.DevPluginCompilationTimeout).To(Equal(consts.DefaultPluginCompilationTimeout))
|
|
})
|
|
|
|
It("should use parsed values for duration fields", func() {
|
|
conf.InitConfig(filepath.Join("testdata", "valid_duration.toml"), false)
|
|
conf.Load(true)
|
|
|
|
configured := 1 * time.Second
|
|
|
|
server := conf.Server
|
|
Expect(server.SessionTimeout).To(Equal(configured))
|
|
Expect(server.SmartPlaylistRefreshDelay).To(Equal(configured))
|
|
Expect(server.DefaultShareExpiration).To(Equal(configured))
|
|
Expect(server.UIPlaybackReportInterval).To(Equal(configured))
|
|
Expect(server.AuthWindowLength).To(Equal(configured))
|
|
Expect(server.Scanner.WatcherWait).To(Equal(configured))
|
|
|
|
Expect(server.DevActivityPanelUpdateRate).To(Equal(configured))
|
|
Expect(server.DevArtworkThrottleBacklogTimeout).To(Equal(configured))
|
|
Expect(server.DevArtistInfoTimeToLive).To(Equal(configured))
|
|
Expect(server.DevAlbumInfoTimeToLive).To(Equal(configured))
|
|
Expect(server.DevInsightsInitialDelay).To(Equal(configured))
|
|
Expect(server.DevPluginCompilationTimeout).To(Equal(configured))
|
|
})
|
|
})
|
|
|
|
var _ = Describe("TLSEnabled", func() {
|
|
BeforeEach(func() {
|
|
DeferCleanup(configtest.SetupConfig())
|
|
})
|
|
|
|
It("is false when neither the certificate nor the key is set", func() {
|
|
Expect(conf.Server.TLSEnabled()).To(BeFalse())
|
|
})
|
|
|
|
It("is true when both the certificate and the key are set", func() {
|
|
conf.Server.TLSCert = "cert.pem"
|
|
conf.Server.TLSKey = "key.pem"
|
|
Expect(conf.Server.TLSEnabled()).To(BeTrue())
|
|
})
|
|
|
|
It("is false when only the certificate is set", func() {
|
|
conf.Server.TLSCert = "cert.pem"
|
|
Expect(conf.Server.TLSEnabled()).To(BeFalse())
|
|
})
|
|
|
|
It("is false when only the key is set", func() {
|
|
conf.Server.TLSKey = "key.pem"
|
|
Expect(conf.Server.TLSEnabled()).To(BeFalse())
|
|
})
|
|
})
|